@oneuptime/common 12.0.25 → 12.0.27

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 (856) hide show
  1. package/Models/AnalyticsModels/Index.ts +2 -0
  2. package/Models/AnalyticsModels/ThreatIntelIndicator.ts +413 -0
  3. package/Models/DatabaseModels/AlertSeverity.ts +4 -1
  4. package/Models/DatabaseModels/CephClusterFeed.ts +625 -0
  5. package/Models/DatabaseModels/CloudResourceFeed.ts +625 -0
  6. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +13 -0
  7. package/Models/DatabaseModels/DetectionRule.ts +9 -3
  8. package/Models/DatabaseModels/DockerHostFeed.ts +625 -0
  9. package/Models/DatabaseModels/DockerSwarmClusterFeed.ts +628 -0
  10. package/Models/DatabaseModels/GoogleSecOpsConnection.ts +9 -3
  11. package/Models/DatabaseModels/HostFeed.ts +625 -0
  12. package/Models/DatabaseModels/IncidentSeverity.ts +4 -1
  13. package/Models/DatabaseModels/Index.ts +22 -0
  14. package/Models/DatabaseModels/KubernetesClusterFeed.ts +628 -0
  15. package/Models/DatabaseModels/Monitor.ts +87 -0
  16. package/Models/DatabaseModels/NetworkDevice.ts +96 -5
  17. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +92 -6
  18. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +301 -31
  19. package/Models/DatabaseModels/NetworkSite.ts +120 -0
  20. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
  21. package/Models/DatabaseModels/PodmanHostFeed.ts +625 -0
  22. package/Models/DatabaseModels/ProxmoxClusterFeed.ts +626 -0
  23. package/Models/DatabaseModels/ScheduledMaintenance.ts +67 -0
  24. package/Models/DatabaseModels/ServiceFeed.ts +625 -0
  25. package/Models/DatabaseModels/TelemetrySourceMap.ts +474 -0
  26. package/Models/DatabaseModels/ThreatIntelFeed.ts +758 -0
  27. package/Models/DatabaseModels/UserOnCallLog.ts +1 -0
  28. package/Models/DatabaseModels/UserTotpAuth.ts +1 -0
  29. package/Models/DatabaseModels/UserWebAuthn.ts +1 -0
  30. package/Server/API/AlertAPI.ts +23 -1
  31. package/Server/API/BaseAPI.ts +14 -3
  32. package/Server/API/IncidentAPI.ts +41 -1
  33. package/Server/API/IncidentEpisodeAPI.ts +23 -1
  34. package/Server/API/MicrosoftTeamsAPI.ts +31 -2
  35. package/Server/API/ScheduledMaintenanceAPI.ts +23 -1
  36. package/Server/API/SlackAPI.ts +48 -0
  37. package/Server/API/TelemetryAPI.ts +167 -42
  38. package/Server/API/UserOnCallLogTimelineAPI.ts +75 -61
  39. package/Server/API/UserTotpAuthAPI.ts +67 -1
  40. package/Server/API/UserWebAuthnAPI.ts +44 -1
  41. package/Server/Infrastructure/Postgres/SchemaMigrations/1787923136162-MigrationName.ts +57 -0
  42. package/Server/Infrastructure/Postgres/SchemaMigrations/1789600000000-AddAutoProvisionedNetworkDeviceMonitors.ts +49 -0
  43. package/Server/Infrastructure/Postgres/SchemaMigrations/1789700000000-AddNetworkScaleIndexes.ts +129 -0
  44. package/Server/Infrastructure/Postgres/SchemaMigrations/1789800000000-WidenSecurityEventLastErrorColumns.ts +54 -0
  45. package/Server/Infrastructure/Postgres/SchemaMigrations/1789900000000-AddNetworkSiteRollupPolicyAndMaintenance.ts +64 -0
  46. package/Server/Infrastructure/Postgres/SchemaMigrations/1790000000000-AddTelemetrySourceMap.ts +64 -0
  47. package/Server/Infrastructure/Postgres/SchemaMigrations/1790000000001-AddSnmpConfigsToNetworkDeviceDiscoveryScan.ts +72 -0
  48. package/Server/Infrastructure/Postgres/SchemaMigrations/1790003445000-AddSnmpEnabledToNetworkDeviceDiscoveryScan.ts +31 -0
  49. package/Server/Infrastructure/Postgres/SchemaMigrations/1790100000000-AddResourceActivityFeeds.ts +489 -0
  50. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +18 -0
  51. package/Server/Middleware/IdentityRateLimit.ts +53 -3
  52. package/Server/Services/AIService.ts +181 -41
  53. package/Server/Services/CephClusterFeedService.ts +99 -0
  54. package/Server/Services/CephClusterLabelRuleEngineService.ts +29 -0
  55. package/Server/Services/CephClusterOwnerRuleEngineService.ts +23 -0
  56. package/Server/Services/CephClusterOwnerTeamService.ts +129 -0
  57. package/Server/Services/CephClusterOwnerUserService.ts +123 -0
  58. package/Server/Services/CephClusterService.ts +223 -2
  59. package/Server/Services/CloudResourceFeedService.ts +99 -0
  60. package/Server/Services/CloudResourceLabelRuleEngineService.ts +30 -0
  61. package/Server/Services/CloudResourceOwnerRuleEngineService.ts +33 -0
  62. package/Server/Services/CloudResourceOwnerTeamService.ts +129 -0
  63. package/Server/Services/CloudResourceOwnerUserService.ts +123 -0
  64. package/Server/Services/CloudResourceService.ts +227 -2
  65. package/Server/Services/DatabaseService.ts +265 -0
  66. package/Server/Services/DockerHostFeedService.ts +99 -0
  67. package/Server/Services/DockerHostLabelRuleEngineService.ts +29 -0
  68. package/Server/Services/DockerHostOwnerRuleEngineService.ts +23 -0
  69. package/Server/Services/DockerHostOwnerTeamService.ts +129 -0
  70. package/Server/Services/DockerHostOwnerUserService.ts +123 -0
  71. package/Server/Services/DockerHostService.ts +227 -2
  72. package/Server/Services/DockerSwarmClusterFeedService.ts +102 -0
  73. package/Server/Services/DockerSwarmClusterLabelRuleEngineService.ts +30 -0
  74. package/Server/Services/DockerSwarmClusterOwnerRuleEngineService.ts +24 -0
  75. package/Server/Services/DockerSwarmClusterOwnerTeamService.ts +133 -0
  76. package/Server/Services/DockerSwarmClusterOwnerUserService.ts +127 -0
  77. package/Server/Services/DockerSwarmClusterService.ts +229 -2
  78. package/Server/Services/EnterpriseLicenseService.ts +15 -7
  79. package/Server/Services/ExceptionAggregationService.ts +12 -2
  80. package/Server/Services/HostFeedService.ts +99 -0
  81. package/Server/Services/HostLabelRuleEngineService.ts +29 -0
  82. package/Server/Services/HostOwnerRuleEngineService.ts +23 -0
  83. package/Server/Services/HostOwnerTeamService.ts +129 -0
  84. package/Server/Services/HostOwnerUserService.ts +123 -0
  85. package/Server/Services/HostService.ts +215 -2
  86. package/Server/Services/IncidentService.ts +11 -0
  87. package/Server/Services/Index.ts +24 -0
  88. package/Server/Services/KubernetesClusterFeedService.ts +99 -0
  89. package/Server/Services/KubernetesClusterLabelRuleEngineService.ts +30 -0
  90. package/Server/Services/KubernetesClusterOwnerRuleEngineService.ts +24 -0
  91. package/Server/Services/KubernetesClusterOwnerTeamService.ts +133 -0
  92. package/Server/Services/KubernetesClusterOwnerUserService.ts +127 -0
  93. package/Server/Services/KubernetesClusterService.ts +231 -2
  94. package/Server/Services/LogAggregationService.ts +23 -280
  95. package/Server/Services/MetricAggregationService.ts +98 -1
  96. package/Server/Services/MonitorService.ts +177 -13
  97. package/Server/Services/MonitorTemplateService.ts +111 -0
  98. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +777 -86
  99. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +199 -16
  100. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +1055 -14
  101. package/Server/Services/NetworkDeviceService.ts +750 -4
  102. package/Server/Services/NetworkInterfaceService.ts +415 -0
  103. package/Server/Services/NetworkSiteService.ts +485 -52
  104. package/Server/Services/PodmanHostFeedService.ts +99 -0
  105. package/Server/Services/PodmanHostLabelRuleEngineService.ts +29 -0
  106. package/Server/Services/PodmanHostOwnerRuleEngineService.ts +23 -0
  107. package/Server/Services/PodmanHostOwnerTeamService.ts +129 -0
  108. package/Server/Services/PodmanHostOwnerUserService.ts +123 -0
  109. package/Server/Services/PodmanHostService.ts +227 -2
  110. package/Server/Services/ProjectService.ts +16 -10
  111. package/Server/Services/ProxmoxClusterFeedService.ts +99 -0
  112. package/Server/Services/ProxmoxClusterLabelRuleEngineService.ts +30 -0
  113. package/Server/Services/ProxmoxClusterOwnerRuleEngineService.ts +24 -0
  114. package/Server/Services/ProxmoxClusterOwnerTeamService.ts +131 -0
  115. package/Server/Services/ProxmoxClusterOwnerUserService.ts +124 -0
  116. package/Server/Services/ProxmoxClusterService.ts +225 -2
  117. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +61 -0
  118. package/Server/Services/ServiceFeedService.ts +99 -0
  119. package/Server/Services/ServiceLabelRuleEngineService.ts +29 -0
  120. package/Server/Services/ServiceOwnerRuleEngineService.ts +26 -0
  121. package/Server/Services/ServiceOwnerTeamService.ts +129 -0
  122. package/Server/Services/ServiceOwnerUserService.ts +123 -0
  123. package/Server/Services/ServiceService.ts +227 -3
  124. package/Server/Services/StatusPageSubscriberService.ts +7 -0
  125. package/Server/Services/TelemetryAttributeService.ts +13 -3
  126. package/Server/Services/TelemetrySourceMapService.ts +307 -0
  127. package/Server/Services/ThreatIntelFeedService.ts +222 -0
  128. package/Server/Services/ThreatIntelIndicatorService.ts +428 -0
  129. package/Server/Services/TraceAggregationService.ts +101 -11
  130. package/Server/Services/UserService.ts +13 -6
  131. package/Server/Services/UserTwoFactorBackupCodeService.ts +127 -0
  132. package/Server/Services/UserWebhookService.ts +11 -2
  133. package/Server/Static/Brand/README.md +38 -0
  134. package/Server/Static/Brand/favicons/apple-touch-icon.png +0 -0
  135. package/Server/Static/Brand/favicons/favicon-16x16.png +0 -0
  136. package/Server/Static/Brand/favicons/favicon-32x32.png +0 -0
  137. package/Server/Static/Brand/favicons/favicon.ico +0 -0
  138. package/Server/Static/Brand/favicons/safari-pinned-tab.svg +31 -0
  139. package/Server/Static/Brand/hou-wb.svg +1 -0
  140. package/Server/Static/Brand/oneuptime-logo.svg +1 -0
  141. package/Server/Static/Brand/ou-wb.svg +1 -0
  142. package/Server/Static/Vendor/README.md +4 -0
  143. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +13 -3
  144. package/Server/Types/Database/AggregateBy.ts +83 -0
  145. package/Server/Types/Database/AggregateResultUtil.ts +126 -0
  146. package/Server/Types/Database/JSONColumnQuery.ts +18 -0
  147. package/Server/Types/Database/QueryHelper.ts +40 -0
  148. package/Server/Types/Database/QueryUtil.ts +26 -0
  149. package/Server/Types/Workflow/Components/API/Utils.ts +10 -1
  150. package/Server/Types/Workflow/Components/JavaScript.ts +8 -0
  151. package/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.ts +9 -0
  152. package/Server/Utils/AI/SRE/InvestigationGrader.ts +18 -0
  153. package/Server/Utils/AnalyticsDatabase/AttributeFilterStatement.ts +422 -0
  154. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +176 -0
  155. package/Server/Utils/DataSource/HttpFetch.ts +18 -0
  156. package/Server/Utils/Database/RelationIdUtil.ts +42 -0
  157. package/Server/Utils/Marketing/MarketingEventUtil.ts +38 -9
  158. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +34 -3
  159. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +9 -1
  160. package/Server/Utils/Monitor/Criteria/SyntheticMonitor.ts +28 -10
  161. package/Server/Utils/Monitor/MonitorAlert.ts +541 -332
  162. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +330 -64
  163. package/Server/Utils/Monitor/MonitorIncident.ts +640 -434
  164. package/Server/Utils/Monitor/MonitorResource.ts +226 -49
  165. package/Server/Utils/Monitor/NetworkDeviceWalkUtil.ts +46 -3
  166. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +23 -102
  167. package/Server/Utils/Monitor/PerEntityCriteriaFanOut.ts +329 -0
  168. package/Server/Utils/NetworkDevice/DeviceHealthAggregation.ts +427 -0
  169. package/Server/Utils/NetworkSite/NetworkSiteMaintenanceSuppression.ts +310 -0
  170. package/Server/Utils/OnCallNotificationContext.ts +686 -0
  171. package/Server/Utils/PrivateNetworkWebhookConfig.ts +549 -0
  172. package/Server/Utils/ResourceFeed/ResourceFeedUtil.ts +139 -0
  173. package/Server/Utils/Response.ts +11 -1
  174. package/Server/Utils/SSRFProtection.ts +230 -60
  175. package/Server/Utils/SecurityEvent/ConnectorErrorMessage.ts +106 -0
  176. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +84 -284
  177. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +47 -20
  178. package/Server/Utils/SecurityEvent/SecurityEventAlerting.ts +318 -0
  179. package/Server/Utils/SecurityEvent/ThreatIntel/TaxiiClient.ts +199 -0
  180. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelEnricher.ts +252 -0
  181. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.ts +739 -0
  182. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.ts +533 -0
  183. package/Server/Utils/StartServer.ts +83 -56
  184. package/Server/Utils/StatusPageSubscriberWebhook.ts +3 -0
  185. package/Server/Utils/Telemetry/SourceMapResolver.ts +480 -0
  186. package/Server/Utils/TwoFactorBackupCodeNotification.ts +106 -0
  187. package/Server/Utils/VM/VMAPI.ts +3 -0
  188. package/Server/Utils/VM/VMRunner.ts +27 -1
  189. package/Server/Utils/VendorAssets.ts +42 -0
  190. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +136 -6
  191. package/Server/Views/AcknowledgeUserOnCallNotification.ejs +11 -9
  192. package/Server/Views/Partials/Head.ejs +9 -9
  193. package/Server/Views/Partials/OnCallNotificationDetails.ejs +48 -0
  194. package/Server/Views/ViewMessage.ejs +11 -9
  195. package/Tests/App/Accounts/LoginTwoFactorRecovery.test.tsx +972 -0
  196. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +401 -0
  197. package/Tests/App/Dashboard/DiscoveryScanEditForm.test.tsx +707 -0
  198. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +1908 -18
  199. package/Tests/App/Dashboard/ResourceFeed.test.tsx +230 -0
  200. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +216 -0
  201. package/Tests/App/Dashboard/TopologyAddToMonitoring.test.tsx +192 -0
  202. package/Tests/App/Dashboard/WorkspaceNotificationRuleConditions.test.tsx +439 -0
  203. package/Tests/Models/DatabaseModels/DateColumnDeserialization.test.ts +182 -0
  204. package/Tests/Models/DatabaseModels/RelationColumnModelTypeCoverage.test.ts +82 -0
  205. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +398 -0
  206. package/Tests/Models/DiscoveryScanNameColumn.test.ts +151 -16
  207. package/Tests/Models/DiscoveryScanSnmpEnabledColumn.test.ts +593 -0
  208. package/Tests/Models/NetworkDeviceCreateContract.test.ts +102 -0
  209. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +607 -0
  210. package/Tests/Server/API/AIGenerationRequestBudget.test.ts +355 -0
  211. package/Tests/Server/API/AIKillSwitchBackstop.test.ts +602 -0
  212. package/Tests/Server/API/BaseAPIUpdateDateColumns.test.ts +154 -0
  213. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +335 -0
  214. package/Tests/Server/API/UserOnCallLogTimelineAcknowledgePage.test.ts +611 -0
  215. package/Tests/Server/API/UserTotpAuthAPI.test.ts +498 -4
  216. package/Tests/Server/API/UserWebAuthnBackupCodeMinting.test.ts +682 -0
  217. package/Tests/Server/Infrastructure/ExampleDockerfiles.test.ts +595 -0
  218. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +9 -0
  219. package/Tests/Server/Services/AIServiceProjectAccess.test.ts +6 -0
  220. package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +18 -0
  221. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +590 -0
  222. package/Tests/Server/Services/DatabaseServiceUpdateWriteRouting.test.ts +12 -0
  223. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +162 -26
  224. package/Tests/Server/Services/EnterpriseLicenseServiceCreate.test.ts +292 -0
  225. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +119 -12
  226. package/Tests/Server/Services/LogAggregationAttributeOperators.test.ts +109 -0
  227. package/Tests/Server/Services/MetricAggregationAttributeOperators.test.ts +170 -0
  228. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +383 -0
  229. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +997 -3
  230. package/Tests/Server/Services/NetworkDeviceAutoImportRuleService.test.ts +830 -0
  231. package/Tests/Server/Services/NetworkDeviceAutoMonitorLifecycle.test.ts +528 -0
  232. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +3141 -1
  233. package/Tests/Server/Services/NetworkDeviceRegisteredHostnames.test.ts +1023 -0
  234. package/Tests/Server/Services/NetworkDeviceSiteAssignmentRuleSkip.test.ts +728 -0
  235. package/Tests/Server/Services/NetworkInterfaceServiceUpsert.test.ts +795 -0
  236. package/Tests/Server/Services/NetworkSiteRollupPolicyAndMaintenance.test.ts +903 -0
  237. package/Tests/Server/Services/NetworkSiteService.test.ts +165 -24
  238. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +5 -3
  239. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +5 -2
  240. package/Tests/Server/Services/ResourceCreationFeedWrites.test.ts +304 -0
  241. package/Tests/Server/Services/ResourceFeedServices.test.ts +269 -0
  242. package/Tests/Server/Services/ResourceOwnerFeedWrites.test.ts +245 -0
  243. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +792 -0
  244. package/Tests/Server/Services/TelemetrySourceMapService.test.ts +483 -0
  245. package/Tests/Server/Services/ThreatIntelFeedService.test.ts +351 -0
  246. package/Tests/Server/Services/ThreatIntelIndicatorService.test.ts +366 -0
  247. package/Tests/Server/Services/TraceAggregationAttributeOperators.test.ts +507 -0
  248. package/Tests/Server/Services/UserTwoFactorBackupCodeGenerateIfNone.test.ts +705 -0
  249. package/Tests/Server/Services/WorkspaceNotificationRuleOnCallDutyPolicy.test.ts +306 -0
  250. package/Tests/Server/Types/Database/AggregateResultUtil.test.ts +386 -0
  251. package/Tests/Server/Types/Database/Permissions/SelectRejectionMessageContract.test.ts +424 -0
  252. package/Tests/Server/Types/Database/QueryHelperWildcard.test.ts +142 -0
  253. package/Tests/Server/Types/Workflow/Components/ApiComponentPrivateNetwork.test.ts +163 -0
  254. package/Tests/Server/Types/Workflow/Components/JavaScriptPrivateNetwork.test.ts +124 -0
  255. package/Tests/Server/Utils/AI/CodeFixAgentCompletion.test.ts +5 -0
  256. package/Tests/Server/Utils/AI/InvestigationGrader.test.ts +39 -1
  257. package/Tests/Server/Utils/AnalyticsDatabase/AttributeFilterStatement.test.ts +338 -0
  258. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWildcard.test.ts +383 -0
  259. package/Tests/Server/Utils/Marketing/EmittedMarketingEvent.ts +23 -0
  260. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +51 -0
  261. package/Tests/Server/Utils/Monitor/Criteria/SyntheticMonitorCriteria.test.ts +259 -0
  262. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +557 -0
  263. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +12 -2
  264. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkLogWritePath.test.ts +404 -0
  265. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +8 -2
  266. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +125 -98
  267. package/Tests/Server/Utils/Monitor/PerEntityCriteriaFanOut.test.ts +444 -0
  268. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1112 -0
  269. package/Tests/Server/Utils/NetworkDevice/DeviceHealthAggregation.test.ts +717 -0
  270. package/Tests/Server/Utils/NetworkDevice/RulePatternValidator.test.ts +78 -0
  271. package/Tests/Server/Utils/NetworkSite/NetworkSiteMaintenanceSuppression.test.ts +363 -0
  272. package/Tests/Server/Utils/OfflineAssetHygiene.test.ts +35 -0
  273. package/Tests/Server/Utils/OnCallNotificationContext.test.ts +908 -0
  274. package/Tests/Server/Utils/PrivateNetworkWebhookConfig.test.ts +423 -0
  275. package/Tests/Server/Utils/ResourceFeed/ResourceFeedUtil.test.ts +191 -0
  276. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +597 -0
  277. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +819 -0
  278. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +381 -0
  279. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +417 -0
  280. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +819 -0
  281. package/Tests/Server/Utils/SecurityEvent/SecurityEventAlerting.test.ts +246 -0
  282. package/Tests/Server/Utils/SecurityEvent/ThreatIntel/TaxiiClient.test.ts +282 -0
  283. package/Tests/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelEnricher.test.ts +312 -0
  284. package/Tests/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.test.ts +854 -0
  285. package/Tests/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.test.ts +437 -0
  286. package/Tests/Server/Utils/StartServerErrorStatus.test.ts +225 -0
  287. package/Tests/Server/Utils/StatusPage/MonitorRulePatternValidator.test.ts +62 -0
  288. package/Tests/Server/Utils/StatusPageSubscriberWebhookPrivateNetwork.test.ts +119 -0
  289. package/Tests/Server/Utils/Telemetry/SourceMapResolver.test.ts +542 -0
  290. package/Tests/Server/Utils/TwoFactorBackupCodeNotification.test.ts +272 -0
  291. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +111 -0
  292. package/Tests/Server/Utils/VendorAssets.test.ts +117 -0
  293. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +101 -4
  294. package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstallStateDiagnostics.test.ts +922 -0
  295. package/Tests/Server/Utils/Workspace/MicrosoftTeamsRosterDiagnosticMessages.test.ts +642 -0
  296. package/Tests/Server/Views/OnCallAcknowledgePage.test.ts +430 -0
  297. package/Tests/Types/API/HostnameAuthorityParsing.test.ts +1 -0
  298. package/Tests/Types/API/URLMailtoQueryString.test.ts +439 -0
  299. package/Tests/Types/API/URLQueryString.test.ts +481 -0
  300. package/Tests/Types/API/URLRouteQuerySeparation.test.ts +491 -0
  301. package/Tests/Types/BaseDatabase/Wildcard.test.ts +163 -0
  302. package/Tests/Types/BaseDatabase/WildcardPattern.test.ts +149 -0
  303. package/Tests/Types/Database/DateColumnValue.test.ts +265 -0
  304. package/Tests/Types/DateToIsoStringOrNull.test.ts +67 -0
  305. package/Tests/Types/Log/LogQueryToFilter.test.ts +218 -43
  306. package/Tests/Types/Monitor/KubernetesTemplateGroupByKeys.test.ts +225 -0
  307. package/Tests/Types/Monitor/TemplateGroupByKeys.test.ts +296 -0
  308. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +102 -0
  309. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +145 -0
  310. package/Tests/Types/NetworkSite/SiteHealthRollupPolicy.test.ts +58 -0
  311. package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +15 -0
  312. package/Tests/Types/Telemetry/TelemetrySearchQuery.test.ts +620 -0
  313. package/Tests/Types/WhatsApp/WhatsAppTemplates.test.ts +110 -0
  314. package/Tests/Types/Workspace/NotificationRuleConditionUtil.test.ts +76 -6
  315. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +16 -4
  316. package/Tests/Types/Workspace/NotificationRules/OnCallDutyPolicyConditions.test.ts +669 -0
  317. package/Tests/UI/Components/DictionaryFilterOperatorWildcard.test.ts +175 -0
  318. package/Tests/UI/Components/IconOpticalSize.test.tsx +377 -0
  319. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +393 -0
  320. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +248 -0
  321. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +205 -0
  322. package/Tests/UI/Components/UseHistogramZoom.test.tsx +262 -0
  323. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardInstallGuidance.test.tsx +332 -0
  324. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsInstallGuidanceContent.test.tsx +643 -0
  325. package/Tests/Utils/Monitor/InterfaceInventoryUtil.test.ts +824 -0
  326. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +626 -0
  327. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +718 -0
  328. package/Tests/Utils/NetworkDevice/DeviceHealthStateUtil.test.ts +59 -0
  329. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +287 -2
  330. package/Tests/Utils/NetworkDiscovery/DiscoveryImportEligibility.test.ts +73 -0
  331. package/Tests/Utils/NetworkDiscovery/PingMonitorBuilder.test.ts +317 -0
  332. package/Tests/Utils/NetworkDiscovery/RescanIntervalUtil.test.ts +262 -0
  333. package/Tests/Utils/NetworkDiscovery/ScanModeUtil.test.ts +474 -0
  334. package/Tests/Utils/NetworkDiscovery/SnmpScanConfigUtil.test.ts +2442 -0
  335. package/Tests/Utils/NetworkSite/SiteMaintenanceUtil.test.ts +188 -0
  336. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +522 -0
  337. package/Tests/Utils/NetworkSite/SiteUptimeUtil.test.ts +694 -0
  338. package/Tests/Utils/SecurityEvent/ThreatIntel/StixPatternParser.test.ts +281 -0
  339. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +87 -67
  340. package/Types/API/URL.ts +100 -28
  341. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +8 -0
  342. package/Types/BaseDatabase/NotWildcard.ts +86 -0
  343. package/Types/BaseDatabase/Wildcard.ts +89 -0
  344. package/Types/BaseDatabase/WildcardPattern.ts +176 -0
  345. package/Types/Database/DateColumnValue.ts +184 -0
  346. package/Types/Database/TableColumn.ts +40 -5
  347. package/Types/Date.ts +37 -0
  348. package/Types/Email/EmailTemplateType.ts +1 -0
  349. package/Types/JSON.ts +8 -0
  350. package/Types/Log/LogQueryToFilter.ts +167 -129
  351. package/Types/Monitor/DockerAlertTemplates.ts +43 -12
  352. package/Types/Monitor/HostAlertTemplates.ts +31 -3
  353. package/Types/Monitor/KubernetesAlertTemplates.ts +98 -0
  354. package/Types/Monitor/MonitorStep.ts +49 -0
  355. package/Types/Monitor/PodmanAlertTemplates.ts +39 -12
  356. package/Types/Monitor/SnmpMonitor/CdpNeighbor.ts +14 -0
  357. package/Types/Monitor/SnmpMonitor/LldpNeighbor.ts +11 -0
  358. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +11 -4
  359. package/Types/NetworkAutomation/RuleRunResult.ts +191 -2
  360. package/Types/NetworkSite/SiteHealthRollupPolicy.ts +96 -0
  361. package/Types/Permission.ts +411 -0
  362. package/Types/Probe/ProbeApiIngestResponse.ts +42 -0
  363. package/Types/SecurityEvent/ThreatIntelConstants.ts +118 -0
  364. package/Types/SerializableObjectDictionary.ts +8 -0
  365. package/Types/Telemetry/SourceMap.ts +56 -0
  366. package/Types/Telemetry/TelemetrySearchQuery.ts +949 -0
  367. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +12 -1
  368. package/UI/Components/Charts/Utils/useHistogramZoom.ts +101 -0
  369. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +83 -0
  370. package/UI/Components/Icon/Icon.tsx +28 -1
  371. package/UI/Components/LogsViewer/LogsViewer.tsx +19 -3
  372. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +26 -4
  373. package/UI/Components/LogsViewer/components/LogSearchHelp.tsx +34 -16
  374. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +52 -1
  375. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +18 -2
  376. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +52 -1
  377. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +15 -2
  378. package/Utils/Metrics/MetricExplorerUrl.ts +77 -15
  379. package/Utils/Monitor/InterfaceInventoryUtil.ts +251 -0
  380. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +333 -0
  381. package/Utils/Monitor/NetworkTopologyUtil.ts +249 -28
  382. package/Utils/NetworkDevice/DeviceHealthStateUtil.ts +14 -6
  383. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +2 -0
  384. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +49 -18
  385. package/Utils/NetworkDiscovery/PingMonitorBuilder.ts +215 -0
  386. package/Utils/NetworkDiscovery/RescanIntervalUtil.ts +148 -0
  387. package/Utils/NetworkDiscovery/ScanModeUtil.ts +105 -0
  388. package/Utils/NetworkDiscovery/SnmpScanConfigUtil.ts +827 -0
  389. package/Utils/NetworkSite/SiteMaintenanceUtil.ts +110 -0
  390. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +350 -19
  391. package/Utils/NetworkSite/SiteUptimeUtil.ts +422 -21
  392. package/Utils/SecurityEvent/ThreatIntel/StixPatternParser.ts +383 -0
  393. package/Utils/Telemetry/CrossSignalScope.ts +37 -39
  394. package/build/dist/Models/AnalyticsModels/Index.js +2 -0
  395. package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
  396. package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js +325 -0
  397. package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js.map +1 -0
  398. package/build/dist/Models/DatabaseModels/AlertSeverity.js +4 -1
  399. package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
  400. package/build/dist/Models/DatabaseModels/CephClusterFeed.js +648 -0
  401. package/build/dist/Models/DatabaseModels/CephClusterFeed.js.map +1 -0
  402. package/build/dist/Models/DatabaseModels/CloudResourceFeed.js +648 -0
  403. package/build/dist/Models/DatabaseModels/CloudResourceFeed.js.map +1 -0
  404. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +11 -0
  405. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  406. package/build/dist/Models/DatabaseModels/DetectionRule.js +9 -3
  407. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  408. package/build/dist/Models/DatabaseModels/DockerHostFeed.js +648 -0
  409. package/build/dist/Models/DatabaseModels/DockerHostFeed.js.map +1 -0
  410. package/build/dist/Models/DatabaseModels/DockerSwarmClusterFeed.js +648 -0
  411. package/build/dist/Models/DatabaseModels/DockerSwarmClusterFeed.js.map +1 -0
  412. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +9 -3
  413. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -1
  414. package/build/dist/Models/DatabaseModels/HostFeed.js +648 -0
  415. package/build/dist/Models/DatabaseModels/HostFeed.js.map +1 -0
  416. package/build/dist/Models/DatabaseModels/IncidentSeverity.js +4 -1
  417. package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
  418. package/build/dist/Models/DatabaseModels/Index.js +22 -0
  419. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  420. package/build/dist/Models/DatabaseModels/KubernetesClusterFeed.js +648 -0
  421. package/build/dist/Models/DatabaseModels/KubernetesClusterFeed.js.map +1 -0
  422. package/build/dist/Models/DatabaseModels/Monitor.js +81 -0
  423. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  424. package/build/dist/Models/DatabaseModels/NetworkDevice.js +103 -7
  425. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  426. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +91 -6
  427. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  428. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +276 -31
  429. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  430. package/build/dist/Models/DatabaseModels/NetworkSite.js +124 -1
  431. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  432. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  433. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  434. package/build/dist/Models/DatabaseModels/PodmanHostFeed.js +648 -0
  435. package/build/dist/Models/DatabaseModels/PodmanHostFeed.js.map +1 -0
  436. package/build/dist/Models/DatabaseModels/ProxmoxClusterFeed.js +648 -0
  437. package/build/dist/Models/DatabaseModels/ProxmoxClusterFeed.js.map +1 -0
  438. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +66 -0
  439. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  440. package/build/dist/Models/DatabaseModels/ServiceFeed.js +648 -0
  441. package/build/dist/Models/DatabaseModels/ServiceFeed.js.map +1 -0
  442. package/build/dist/Models/DatabaseModels/TelemetrySourceMap.js +496 -0
  443. package/build/dist/Models/DatabaseModels/TelemetrySourceMap.js.map +1 -0
  444. package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js +800 -0
  445. package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js.map +1 -0
  446. package/build/dist/Models/DatabaseModels/UserOnCallLog.js +1 -0
  447. package/build/dist/Models/DatabaseModels/UserOnCallLog.js.map +1 -1
  448. package/build/dist/Models/DatabaseModels/UserTotpAuth.js +1 -0
  449. package/build/dist/Models/DatabaseModels/UserTotpAuth.js.map +1 -1
  450. package/build/dist/Models/DatabaseModels/UserWebAuthn.js +1 -0
  451. package/build/dist/Models/DatabaseModels/UserWebAuthn.js.map +1 -1
  452. package/build/dist/Server/API/AlertAPI.js +18 -1
  453. package/build/dist/Server/API/AlertAPI.js.map +1 -1
  454. package/build/dist/Server/API/BaseAPI.js +8 -1
  455. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  456. package/build/dist/Server/API/IncidentAPI.js +35 -1
  457. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  458. package/build/dist/Server/API/IncidentEpisodeAPI.js +18 -1
  459. package/build/dist/Server/API/IncidentEpisodeAPI.js.map +1 -1
  460. package/build/dist/Server/API/MicrosoftTeamsAPI.js +30 -2
  461. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  462. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +18 -1
  463. package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -1
  464. package/build/dist/Server/API/SlackAPI.js +45 -0
  465. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  466. package/build/dist/Server/API/TelemetryAPI.js +100 -35
  467. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  468. package/build/dist/Server/API/UserOnCallLogTimelineAPI.js +64 -59
  469. package/build/dist/Server/API/UserOnCallLogTimelineAPI.js.map +1 -1
  470. package/build/dist/Server/API/UserTotpAuthAPI.js +65 -1
  471. package/build/dist/Server/API/UserTotpAuthAPI.js.map +1 -1
  472. package/build/dist/Server/API/UserWebAuthnAPI.js +42 -1
  473. package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
  474. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787923136162-MigrationName.js +26 -0
  475. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787923136162-MigrationName.js.map +1 -0
  476. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789600000000-AddAutoProvisionedNetworkDeviceMonitors.js +22 -0
  477. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789600000000-AddAutoProvisionedNetworkDeviceMonitors.js.map +1 -0
  478. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789700000000-AddNetworkScaleIndexes.js +88 -0
  479. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789700000000-AddNetworkScaleIndexes.js.map +1 -0
  480. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789800000000-WidenSecurityEventLastErrorColumns.js +39 -0
  481. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789800000000-WidenSecurityEventLastErrorColumns.js.map +1 -0
  482. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789900000000-AddNetworkSiteRollupPolicyAndMaintenance.js +34 -0
  483. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789900000000-AddNetworkSiteRollupPolicyAndMaintenance.js.map +1 -0
  484. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790000000000-AddTelemetrySourceMap.js +32 -0
  485. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790000000000-AddTelemetrySourceMap.js.map +1 -0
  486. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790000000001-AddSnmpConfigsToNetworkDeviceDiscoveryScan.js +61 -0
  487. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790000000001-AddSnmpConfigsToNetworkDeviceDiscoveryScan.js.map +1 -0
  488. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790003445000-AddSnmpEnabledToNetworkDeviceDiscoveryScan.js +23 -0
  489. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790003445000-AddSnmpEnabledToNetworkDeviceDiscoveryScan.js.map +1 -0
  490. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790100000000-AddResourceActivityFeeds.js +179 -0
  491. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790100000000-AddResourceActivityFeeds.js.map +1 -0
  492. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +18 -0
  493. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  494. package/build/dist/Server/Middleware/IdentityRateLimit.js +40 -3
  495. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  496. package/build/dist/Server/Services/AIService.js +177 -30
  497. package/build/dist/Server/Services/AIService.js.map +1 -1
  498. package/build/dist/Server/Services/CephClusterFeedService.js +89 -0
  499. package/build/dist/Server/Services/CephClusterFeedService.js.map +1 -0
  500. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js +27 -3
  501. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js.map +1 -1
  502. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js +21 -0
  503. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js.map +1 -1
  504. package/build/dist/Server/Services/CephClusterOwnerTeamService.js +120 -0
  505. package/build/dist/Server/Services/CephClusterOwnerTeamService.js.map +1 -1
  506. package/build/dist/Server/Services/CephClusterOwnerUserService.js +113 -0
  507. package/build/dist/Server/Services/CephClusterOwnerUserService.js.map +1 -1
  508. package/build/dist/Server/Services/CephClusterService.js +175 -1
  509. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  510. package/build/dist/Server/Services/CloudResourceFeedService.js +89 -0
  511. package/build/dist/Server/Services/CloudResourceFeedService.js.map +1 -0
  512. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js +27 -3
  513. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js.map +1 -1
  514. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js +28 -3
  515. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js.map +1 -1
  516. package/build/dist/Server/Services/CloudResourceOwnerTeamService.js +120 -0
  517. package/build/dist/Server/Services/CloudResourceOwnerTeamService.js.map +1 -1
  518. package/build/dist/Server/Services/CloudResourceOwnerUserService.js +113 -0
  519. package/build/dist/Server/Services/CloudResourceOwnerUserService.js.map +1 -1
  520. package/build/dist/Server/Services/CloudResourceService.js +177 -1
  521. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  522. package/build/dist/Server/Services/DatabaseService.js +201 -0
  523. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  524. package/build/dist/Server/Services/DockerHostFeedService.js +89 -0
  525. package/build/dist/Server/Services/DockerHostFeedService.js.map +1 -0
  526. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js +27 -3
  527. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js.map +1 -1
  528. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js +21 -0
  529. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js.map +1 -1
  530. package/build/dist/Server/Services/DockerHostOwnerTeamService.js +120 -0
  531. package/build/dist/Server/Services/DockerHostOwnerTeamService.js.map +1 -1
  532. package/build/dist/Server/Services/DockerHostOwnerUserService.js +113 -0
  533. package/build/dist/Server/Services/DockerHostOwnerUserService.js.map +1 -1
  534. package/build/dist/Server/Services/DockerHostService.js +177 -1
  535. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  536. package/build/dist/Server/Services/DockerSwarmClusterFeedService.js +90 -0
  537. package/build/dist/Server/Services/DockerSwarmClusterFeedService.js.map +1 -0
  538. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js +27 -3
  539. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js.map +1 -1
  540. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js +21 -0
  541. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js.map +1 -1
  542. package/build/dist/Server/Services/DockerSwarmClusterOwnerTeamService.js +120 -0
  543. package/build/dist/Server/Services/DockerSwarmClusterOwnerTeamService.js.map +1 -1
  544. package/build/dist/Server/Services/DockerSwarmClusterOwnerUserService.js +113 -0
  545. package/build/dist/Server/Services/DockerSwarmClusterOwnerUserService.js.map +1 -1
  546. package/build/dist/Server/Services/DockerSwarmClusterService.js +175 -1
  547. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  548. package/build/dist/Server/Services/EnterpriseLicenseService.js +40 -30
  549. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  550. package/build/dist/Server/Services/ExceptionAggregationService.js +8 -2
  551. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  552. package/build/dist/Server/Services/HostFeedService.js +89 -0
  553. package/build/dist/Server/Services/HostFeedService.js.map +1 -0
  554. package/build/dist/Server/Services/HostLabelRuleEngineService.js +27 -3
  555. package/build/dist/Server/Services/HostLabelRuleEngineService.js.map +1 -1
  556. package/build/dist/Server/Services/HostOwnerRuleEngineService.js +21 -0
  557. package/build/dist/Server/Services/HostOwnerRuleEngineService.js.map +1 -1
  558. package/build/dist/Server/Services/HostOwnerTeamService.js +120 -0
  559. package/build/dist/Server/Services/HostOwnerTeamService.js.map +1 -1
  560. package/build/dist/Server/Services/HostOwnerUserService.js +113 -0
  561. package/build/dist/Server/Services/HostOwnerUserService.js.map +1 -1
  562. package/build/dist/Server/Services/HostService.js +175 -1
  563. package/build/dist/Server/Services/HostService.js.map +1 -1
  564. package/build/dist/Server/Services/IncidentService.js +10 -0
  565. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  566. package/build/dist/Server/Services/Index.js +24 -0
  567. package/build/dist/Server/Services/Index.js.map +1 -1
  568. package/build/dist/Server/Services/KubernetesClusterFeedService.js +89 -0
  569. package/build/dist/Server/Services/KubernetesClusterFeedService.js.map +1 -0
  570. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js +27 -3
  571. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js.map +1 -1
  572. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js +21 -0
  573. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js.map +1 -1
  574. package/build/dist/Server/Services/KubernetesClusterOwnerTeamService.js +120 -0
  575. package/build/dist/Server/Services/KubernetesClusterOwnerTeamService.js.map +1 -1
  576. package/build/dist/Server/Services/KubernetesClusterOwnerUserService.js +113 -0
  577. package/build/dist/Server/Services/KubernetesClusterOwnerUserService.js.map +1 -1
  578. package/build/dist/Server/Services/KubernetesClusterService.js +177 -1
  579. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  580. package/build/dist/Server/Services/LogAggregationService.js +19 -196
  581. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  582. package/build/dist/Server/Services/MetricAggregationService.js +59 -0
  583. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  584. package/build/dist/Server/Services/MonitorService.js +115 -13
  585. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  586. package/build/dist/Server/Services/MonitorTemplateService.js +86 -0
  587. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  588. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +561 -67
  589. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  590. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +130 -16
  591. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
  592. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +850 -8
  593. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  594. package/build/dist/Server/Services/NetworkDeviceService.js +594 -5
  595. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  596. package/build/dist/Server/Services/NetworkInterfaceService.js +306 -0
  597. package/build/dist/Server/Services/NetworkInterfaceService.js.map +1 -1
  598. package/build/dist/Server/Services/NetworkSiteService.js +408 -47
  599. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  600. package/build/dist/Server/Services/PodmanHostFeedService.js +89 -0
  601. package/build/dist/Server/Services/PodmanHostFeedService.js.map +1 -0
  602. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js +27 -3
  603. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js.map +1 -1
  604. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js +21 -0
  605. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js.map +1 -1
  606. package/build/dist/Server/Services/PodmanHostOwnerTeamService.js +120 -0
  607. package/build/dist/Server/Services/PodmanHostOwnerTeamService.js.map +1 -1
  608. package/build/dist/Server/Services/PodmanHostOwnerUserService.js +113 -0
  609. package/build/dist/Server/Services/PodmanHostOwnerUserService.js.map +1 -1
  610. package/build/dist/Server/Services/PodmanHostService.js +177 -1
  611. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  612. package/build/dist/Server/Services/ProjectService.js +40 -30
  613. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  614. package/build/dist/Server/Services/ProxmoxClusterFeedService.js +89 -0
  615. package/build/dist/Server/Services/ProxmoxClusterFeedService.js.map +1 -0
  616. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js +27 -3
  617. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js.map +1 -1
  618. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js +21 -0
  619. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js.map +1 -1
  620. package/build/dist/Server/Services/ProxmoxClusterOwnerTeamService.js +120 -0
  621. package/build/dist/Server/Services/ProxmoxClusterOwnerTeamService.js.map +1 -1
  622. package/build/dist/Server/Services/ProxmoxClusterOwnerUserService.js +113 -0
  623. package/build/dist/Server/Services/ProxmoxClusterOwnerUserService.js.map +1 -1
  624. package/build/dist/Server/Services/ProxmoxClusterService.js +175 -1
  625. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  626. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +56 -0
  627. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  628. package/build/dist/Server/Services/ServiceFeedService.js +89 -0
  629. package/build/dist/Server/Services/ServiceFeedService.js.map +1 -0
  630. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js +27 -3
  631. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js.map +1 -1
  632. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js +24 -0
  633. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js.map +1 -1
  634. package/build/dist/Server/Services/ServiceOwnerTeamService.js +120 -0
  635. package/build/dist/Server/Services/ServiceOwnerTeamService.js.map +1 -1
  636. package/build/dist/Server/Services/ServiceOwnerUserService.js +113 -0
  637. package/build/dist/Server/Services/ServiceOwnerUserService.js.map +1 -1
  638. package/build/dist/Server/Services/ServiceService.js +173 -2
  639. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  640. package/build/dist/Server/Services/StatusPageSubscriberService.js +7 -0
  641. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  642. package/build/dist/Server/Services/TelemetryAttributeService.js +9 -3
  643. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  644. package/build/dist/Server/Services/TelemetrySourceMapService.js +265 -0
  645. package/build/dist/Server/Services/TelemetrySourceMapService.js.map +1 -0
  646. package/build/dist/Server/Services/ThreatIntelFeedService.js +157 -0
  647. package/build/dist/Server/Services/ThreatIntelFeedService.js.map +1 -0
  648. package/build/dist/Server/Services/ThreatIntelIndicatorService.js +276 -0
  649. package/build/dist/Server/Services/ThreatIntelIndicatorService.js.map +1 -0
  650. package/build/dist/Server/Services/TraceAggregationService.js +62 -6
  651. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  652. package/build/dist/Server/Services/UserService.js +22 -13
  653. package/build/dist/Server/Services/UserService.js.map +1 -1
  654. package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js +120 -0
  655. package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js.map +1 -1
  656. package/build/dist/Server/Services/UserWebhookService.js +11 -2
  657. package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
  658. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +12 -1
  659. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  660. package/build/dist/Server/Types/Database/AggregateBy.js +2 -0
  661. package/build/dist/Server/Types/Database/AggregateBy.js.map +1 -0
  662. package/build/dist/Server/Types/Database/AggregateResultUtil.js +90 -0
  663. package/build/dist/Server/Types/Database/AggregateResultUtil.js.map +1 -0
  664. package/build/dist/Server/Types/Database/JSONColumnQuery.js +14 -0
  665. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  666. package/build/dist/Server/Types/Database/QueryHelper.js +42 -0
  667. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  668. package/build/dist/Server/Types/Database/QueryUtil.js +18 -0
  669. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  670. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +10 -1
  671. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  672. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
  673. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  674. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js +8 -0
  675. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js.map +1 -1
  676. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +15 -0
  677. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js.map +1 -1
  678. package/build/dist/Server/Utils/AnalyticsDatabase/AttributeFilterStatement.js +266 -0
  679. package/build/dist/Server/Utils/AnalyticsDatabase/AttributeFilterStatement.js.map +1 -0
  680. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +134 -0
  681. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  682. package/build/dist/Server/Utils/DataSource/HttpFetch.js +8 -0
  683. package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -1
  684. package/build/dist/Server/Utils/Database/RelationIdUtil.js +31 -0
  685. package/build/dist/Server/Utils/Database/RelationIdUtil.js.map +1 -1
  686. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +26 -8
  687. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  688. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +42 -17
  689. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  690. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +9 -1
  691. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  692. package/build/dist/Server/Utils/Monitor/Criteria/SyntheticMonitor.js +15 -2
  693. package/build/dist/Server/Utils/Monitor/Criteria/SyntheticMonitor.js.map +1 -1
  694. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +386 -271
  695. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  696. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +278 -64
  697. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  698. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +469 -350
  699. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  700. package/build/dist/Server/Utils/Monitor/MonitorResource.js +183 -44
  701. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  702. package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js +46 -3
  703. package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js.map +1 -1
  704. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +18 -91
  705. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  706. package/build/dist/Server/Utils/Monitor/PerEntityCriteriaFanOut.js +194 -0
  707. package/build/dist/Server/Utils/Monitor/PerEntityCriteriaFanOut.js.map +1 -0
  708. package/build/dist/Server/Utils/NetworkDevice/DeviceHealthAggregation.js +332 -0
  709. package/build/dist/Server/Utils/NetworkDevice/DeviceHealthAggregation.js.map +1 -0
  710. package/build/dist/Server/Utils/NetworkSite/NetworkSiteMaintenanceSuppression.js +279 -0
  711. package/build/dist/Server/Utils/NetworkSite/NetworkSiteMaintenanceSuppression.js.map +1 -0
  712. package/build/dist/Server/Utils/OnCallNotificationContext.js +415 -0
  713. package/build/dist/Server/Utils/OnCallNotificationContext.js.map +1 -0
  714. package/build/dist/Server/Utils/PrivateNetworkWebhookConfig.js +424 -0
  715. package/build/dist/Server/Utils/PrivateNetworkWebhookConfig.js.map +1 -0
  716. package/build/dist/Server/Utils/ResourceFeed/ResourceFeedUtil.js +108 -0
  717. package/build/dist/Server/Utils/ResourceFeed/ResourceFeedUtil.js.map +1 -0
  718. package/build/dist/Server/Utils/Response.js +12 -1
  719. package/build/dist/Server/Utils/Response.js.map +1 -1
  720. package/build/dist/Server/Utils/SSRFProtection.js +160 -58
  721. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  722. package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js +89 -0
  723. package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js.map +1 -0
  724. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +75 -234
  725. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  726. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +41 -16
  727. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  728. package/build/dist/Server/Utils/SecurityEvent/SecurityEventAlerting.js +217 -0
  729. package/build/dist/Server/Utils/SecurityEvent/SecurityEventAlerting.js.map +1 -0
  730. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/TaxiiClient.js +103 -0
  731. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/TaxiiClient.js.map +1 -0
  732. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelEnricher.js +168 -0
  733. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelEnricher.js.map +1 -0
  734. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js +527 -0
  735. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js.map +1 -0
  736. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js +376 -0
  737. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js.map +1 -0
  738. package/build/dist/Server/Utils/StartServer.js +70 -44
  739. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  740. package/build/dist/Server/Utils/StatusPageSubscriberWebhook.js +3 -0
  741. package/build/dist/Server/Utils/StatusPageSubscriberWebhook.js.map +1 -1
  742. package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js +327 -0
  743. package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js.map +1 -0
  744. package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js +84 -0
  745. package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js.map +1 -0
  746. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  747. package/build/dist/Server/Utils/VM/VMRunner.js +10 -1
  748. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  749. package/build/dist/Server/Utils/VendorAssets.js +29 -0
  750. package/build/dist/Server/Utils/VendorAssets.js.map +1 -1
  751. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +121 -6
  752. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  753. package/build/dist/Types/API/URL.js +88 -27
  754. package/build/dist/Types/API/URL.js.map +1 -1
  755. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +8 -0
  756. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  757. package/build/dist/Types/BaseDatabase/NotWildcard.js +72 -0
  758. package/build/dist/Types/BaseDatabase/NotWildcard.js.map +1 -0
  759. package/build/dist/Types/BaseDatabase/Wildcard.js +75 -0
  760. package/build/dist/Types/BaseDatabase/Wildcard.js.map +1 -0
  761. package/build/dist/Types/BaseDatabase/WildcardPattern.js +137 -0
  762. package/build/dist/Types/BaseDatabase/WildcardPattern.js.map +1 -0
  763. package/build/dist/Types/Database/DateColumnValue.js +125 -0
  764. package/build/dist/Types/Database/DateColumnValue.js.map +1 -0
  765. package/build/dist/Types/Database/TableColumn.js +18 -2
  766. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  767. package/build/dist/Types/Date.js +31 -0
  768. package/build/dist/Types/Date.js.map +1 -1
  769. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  770. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  771. package/build/dist/Types/JSON.js +2 -0
  772. package/build/dist/Types/JSON.js.map +1 -1
  773. package/build/dist/Types/Log/LogQueryToFilter.js +111 -108
  774. package/build/dist/Types/Log/LogQueryToFilter.js.map +1 -1
  775. package/build/dist/Types/Monitor/DockerAlertTemplates.js +40 -16
  776. package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -1
  777. package/build/dist/Types/Monitor/HostAlertTemplates.js +29 -7
  778. package/build/dist/Types/Monitor/HostAlertTemplates.js.map +1 -1
  779. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +98 -4
  780. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  781. package/build/dist/Types/Monitor/MonitorStep.js +40 -0
  782. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  783. package/build/dist/Types/Monitor/PodmanAlertTemplates.js +36 -16
  784. package/build/dist/Types/Monitor/PodmanAlertTemplates.js.map +1 -1
  785. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +90 -0
  786. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  787. package/build/dist/Types/NetworkSite/SiteHealthRollupPolicy.js +84 -0
  788. package/build/dist/Types/NetworkSite/SiteHealthRollupPolicy.js.map +1 -0
  789. package/build/dist/Types/Permission.js +354 -0
  790. package/build/dist/Types/Permission.js.map +1 -1
  791. package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js +100 -0
  792. package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js.map +1 -0
  793. package/build/dist/Types/SerializableObjectDictionary.js +8 -0
  794. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  795. package/build/dist/Types/Telemetry/SourceMap.js +9 -0
  796. package/build/dist/Types/Telemetry/SourceMap.js.map +1 -0
  797. package/build/dist/Types/Telemetry/TelemetrySearchQuery.js +661 -0
  798. package/build/dist/Types/Telemetry/TelemetrySearchQuery.js.map +1 -0
  799. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +12 -1
  800. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  801. package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js +47 -0
  802. package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js.map +1 -0
  803. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +67 -0
  804. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -1
  805. package/build/dist/UI/Components/Icon/Icon.js +28 -1
  806. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  807. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -3
  808. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  809. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +25 -4
  810. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  811. package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js +36 -16
  812. package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js.map +1 -1
  813. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +33 -5
  814. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  815. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +14 -2
  816. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  817. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +33 -5
  818. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  819. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +14 -2
  820. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  821. package/build/dist/Utils/Metrics/MetricExplorerUrl.js +54 -9
  822. package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
  823. package/build/dist/Utils/Monitor/InterfaceInventoryUtil.js +128 -0
  824. package/build/dist/Utils/Monitor/InterfaceInventoryUtil.js.map +1 -0
  825. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +190 -0
  826. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -0
  827. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +206 -28
  828. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  829. package/build/dist/Utils/NetworkDevice/DeviceHealthStateUtil.js +14 -7
  830. package/build/dist/Utils/NetworkDevice/DeviceHealthStateUtil.js.map +1 -1
  831. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -1
  832. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +34 -18
  833. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -1
  834. package/build/dist/Utils/NetworkDiscovery/PingMonitorBuilder.js +163 -0
  835. package/build/dist/Utils/NetworkDiscovery/PingMonitorBuilder.js.map +1 -0
  836. package/build/dist/Utils/NetworkDiscovery/RescanIntervalUtil.js +99 -0
  837. package/build/dist/Utils/NetworkDiscovery/RescanIntervalUtil.js.map +1 -0
  838. package/build/dist/Utils/NetworkDiscovery/ScanModeUtil.js +85 -0
  839. package/build/dist/Utils/NetworkDiscovery/ScanModeUtil.js.map +1 -0
  840. package/build/dist/Utils/NetworkDiscovery/SnmpScanConfigUtil.js +560 -0
  841. package/build/dist/Utils/NetworkDiscovery/SnmpScanConfigUtil.js.map +1 -0
  842. package/build/dist/Utils/NetworkSite/SiteMaintenanceUtil.js +56 -0
  843. package/build/dist/Utils/NetworkSite/SiteMaintenanceUtil.js.map +1 -0
  844. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +185 -10
  845. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  846. package/build/dist/Utils/NetworkSite/SiteUptimeUtil.js +248 -21
  847. package/build/dist/Utils/NetworkSite/SiteUptimeUtil.js.map +1 -1
  848. package/build/dist/Utils/SecurityEvent/ThreatIntel/StixPatternParser.js +255 -0
  849. package/build/dist/Utils/SecurityEvent/ThreatIntel/StixPatternParser.js.map +1 -0
  850. package/build/dist/Utils/Telemetry/CrossSignalScope.js +27 -30
  851. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  852. package/package.json +2 -1
  853. package/Tests/Types/Log/LogQueryParser.test.ts +0 -186
  854. package/Types/Log/LogQueryParser.ts +0 -252
  855. package/build/dist/Types/Log/LogQueryParser.js +0 -200
  856. package/build/dist/Types/Log/LogQueryParser.js.map +0 -1
@@ -0,0 +1,2442 @@
1
+ import SnmpScanConfigUtil, {
2
+ DiscoveryScanSnmpConfig,
3
+ SnmpScanConfigSource,
4
+ LEGACY_SNMP_CONFIG_ID,
5
+ MAX_SNMP_CONFIGS_PER_SCAN,
6
+ MAX_SNMP_CONFIG_NAME_LENGTH,
7
+ MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH,
8
+ MAX_SNMP_CONFIG_KEY_LENGTH,
9
+ MINIMUM_SNMP_PORT,
10
+ MAXIMUM_SNMP_PORT,
11
+ } from "../../../Utils/NetworkDiscovery/SnmpScanConfigUtil";
12
+ import ScanTargetUtil from "../../../Utils/NetworkDiscovery/ScanTargetUtil";
13
+ import ColumnLength from "../../../Types/Database/ColumnLength";
14
+ import SnmpAuthProtocol from "../../../Types/Monitor/SnmpMonitor/SnmpAuthProtocol";
15
+ import SnmpPrivProtocol from "../../../Types/Monitor/SnmpMonitor/SnmpPrivProtocol";
16
+ import SnmpSecurityLevel from "../../../Types/Monitor/SnmpMonitor/SnmpSecurityLevel";
17
+ import { describe, expect, it } from "@jest/globals";
18
+
19
+ /*
20
+ * Contract under test: the ordered list of SNMP credential sets a discovery
21
+ * scan tries (OneUptime issue #3458).
22
+ *
23
+ * Four layers read this module and every one of them has to get the SAME
24
+ * answer, in a different process, at a different time:
25
+ *
26
+ * - the Dashboard form, which collects the list and shows its errors,
27
+ * - the server's create/update hooks, which validate, normalize and mirror,
28
+ * - the probe, which sweeps a subnet with the list and stamps onto every
29
+ * discovered host WHICH entry answered it,
30
+ * - the import path, which has to rebuild that host's device with THAT
31
+ * entry's credentials — days later, from a row it re-resolves itself.
32
+ *
33
+ * So the properties pinned below are mostly about determinism and about the
34
+ * two directions credentials can leak:
35
+ *
36
+ * 1. resolve() is never empty and never lossy. A scan written before this
37
+ * column existed is described by its nine flattened columns, and those
38
+ * columns ARE its one credential set — resolving them has to produce the
39
+ * identical config on every read, in every process, forever, because a
40
+ * stored `snmpConfigId` of "legacy" is looked up against it.
41
+ * 2. resolve() and readStoredList() answer two DIFFERENT questions. Readers
42
+ * want "what does this scan try?" (never empty); writers want "does this
43
+ * row have its own list, or is it described by its flattened columns?"
44
+ * (honestly null). Conflating them is how a row acquires a list nobody
45
+ * asked for.
46
+ * 3. ids are identity, never position. Duplicate ids are refused and
47
+ * normalizeForStorage mints real ones, because a host found by config #4
48
+ * importing with config #1's community string is the exact bug the id
49
+ * scheme exists to prevent.
50
+ * 4. getConfigLabel() is the ONLY thing about a config that is allowed out
51
+ * into a log line or the scan's statusMessage, so it must never carry a
52
+ * community string or a v3 key. That is a security property, and it is
53
+ * asserted as one below.
54
+ */
55
+
56
+ /*
57
+ * A UUID as ObjectID.generate() spells it. normalizeForStorage mints these for
58
+ * entries that arrive without an id, and the test below checks the shape
59
+ * rather than the value because the whole point is that it is unpredictable.
60
+ */
61
+ const UUID_PATTERN: RegExp =
62
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
63
+
64
+ // The positional id normalizeConfig falls back to when READING a row.
65
+ const POSITIONAL_ID_PATTERN: RegExp = /^config-\d+$/;
66
+
67
+ /*
68
+ * A scan configured the old way: no `snmpConfigs`, nine flattened columns.
69
+ * Every scan that existed before this feature looks like this, and so does
70
+ * every scan written by an API caller that only knows the old fields.
71
+ */
72
+ const LEGACY_SCAN: SnmpScanConfigSource = {
73
+ snmpVersion: "V2c",
74
+ snmpCommunityString: "legacy-community",
75
+ snmpPort: 1161,
76
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
77
+ snmpV3Username: "legacy-user",
78
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA,
79
+ snmpV3AuthKey: "legacy-auth-key",
80
+ snmpV3PrivProtocol: SnmpPrivProtocol.AES,
81
+ snmpV3PrivKey: "legacy-priv-key",
82
+ };
83
+
84
+ /*
85
+ * The nine flattened columns getMirroredLegacyColumns has to write on every
86
+ * save, present or not. Listed here as data so the "always all nine" test is
87
+ * about the count as much as the names.
88
+ */
89
+ const MIRRORED_COLUMN_KEYS: Array<string> = [
90
+ "snmpVersion",
91
+ "snmpCommunityString",
92
+ "snmpPort",
93
+ "snmpV3SecurityLevel",
94
+ "snmpV3Username",
95
+ "snmpV3AuthProtocol",
96
+ "snmpV3AuthKey",
97
+ "snmpV3PrivProtocol",
98
+ "snmpV3PrivKey",
99
+ ];
100
+
101
+ /*
102
+ * The credential fields bounded to ColumnLength.ShortText, paired with the
103
+ * words the validation message uses for each, and listed here as data so the
104
+ * length rule is asserted for EVERY one of them rather than for whichever one
105
+ * a hand-written test happened to pick.
106
+ *
107
+ * These are exactly the fields getMirroredLegacyColumns copies onto a
108
+ * varchar(100) column, plus `id`, which is mirrored nowhere but is stored
109
+ * beside them and read back by findById. `name` is deliberately absent: it has
110
+ * its own constant and its own block above.
111
+ */
112
+ const SHORT_TEXT_CREDENTIAL_FIELDS: Array<
113
+ [keyof DiscoveryScanSnmpConfig, string]
114
+ > = [
115
+ ["id", "id"],
116
+ ["snmpCommunityString", "community string"],
117
+ ["snmpV3Username", "v3 username"],
118
+ ["snmpV3SecurityLevel", "v3 security level"],
119
+ ["snmpV3AuthProtocol", "v3 authentication protocol"],
120
+ ["snmpV3PrivProtocol", "v3 privacy protocol"],
121
+ ];
122
+
123
+ /*
124
+ * The two fields mirrored onto a varchar(500) instead. Keys are localized
125
+ * passphrases, not digests, so they are genuinely longer than a community
126
+ * string and must not be squeezed into the ShortText bound.
127
+ */
128
+ const KEY_CREDENTIAL_FIELDS: Array<[keyof DiscoveryScanSnmpConfig, string]> = [
129
+ ["snmpV3AuthKey", "v3 authentication key"],
130
+ ["snmpV3PrivKey", "v3 privacy key"],
131
+ ];
132
+
133
+ /*
134
+ * A config carrying one field, chosen at runtime from the tables above.
135
+ * Written through a Record cast for the same reason the utility itself does
136
+ * it: a `keyof` index into a struct with mixed value types is not assignable
137
+ * in a literal.
138
+ */
139
+ function configWithField(
140
+ key: keyof DiscoveryScanSnmpConfig,
141
+ value: string,
142
+ ): DiscoveryScanSnmpConfig {
143
+ const config: DiscoveryScanSnmpConfig = {};
144
+
145
+ (config as Record<string, unknown>)[key as string] = value;
146
+
147
+ return config;
148
+ }
149
+
150
+ /*
151
+ * Builds a scan row whose `snmpConfigs` column holds an arbitrary value. The
152
+ * column is jsonb, so an out-of-band writer really can put a string, a number
153
+ * or an array of nonsense in it — the cast is the point of the helper, not a
154
+ * convenience.
155
+ */
156
+ function scanWithConfigs(configs: unknown): SnmpScanConfigSource {
157
+ return {
158
+ snmpConfigs: configs as Array<DiscoveryScanSnmpConfig> | null | undefined,
159
+ };
160
+ }
161
+
162
+ /*
163
+ * Validates a list whose SECOND entry is the interesting one, so every
164
+ * assertion below can check that the message names the offending card by
165
+ * position. With five credential sets on screen, "SNMP v3 Username is
166
+ * required" on its own is not an actionable sentence.
167
+ */
168
+ function errorForSecondConfig(config: unknown): string | null {
169
+ return SnmpScanConfigUtil.getValidationError([
170
+ { id: "first-config" },
171
+ config,
172
+ ]);
173
+ }
174
+
175
+ // A v3 config that passes every rule, used as the base for the negative cases.
176
+ function validAuthPrivConfig(
177
+ overrides: Partial<DiscoveryScanSnmpConfig> = {},
178
+ ): DiscoveryScanSnmpConfig {
179
+ return {
180
+ id: "v3-config",
181
+ name: "Core switches",
182
+ snmpVersion: "V3",
183
+ snmpV3Username: "monitor",
184
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
185
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA256,
186
+ snmpV3AuthKey: "auth-key-value",
187
+ snmpV3PrivProtocol: SnmpPrivProtocol.AES256,
188
+ snmpV3PrivKey: "priv-key-value",
189
+ ...overrides,
190
+ };
191
+ }
192
+
193
+ describe("SnmpScanConfigUtil constants", () => {
194
+ /*
195
+ * The legacy id is a LITERAL, not a minted ObjectID, and it has to stay
196
+ * one: the probe stamps it onto each discovered host and the import path
197
+ * looks it up again in a list resolved separately, in another process,
198
+ * possibly days later. Changing this string orphans every stored result.
199
+ */
200
+ it("uses a stable literal id for the config synthesized from the flattened columns", () => {
201
+ expect(LEGACY_SNMP_CONFIG_ID).toBe("legacy");
202
+ });
203
+
204
+ /*
205
+ * The ceiling is a time budget: a host that answers nothing costs one full
206
+ * SNMP timeout PER config, and the probe abandons a sweep at 90 minutes.
207
+ */
208
+ it("caps a scan at ten SNMP configs, which keeps the worst-case sweep inside the probe deadline", () => {
209
+ expect(MAX_SNMP_CONFIGS_PER_SCAN).toBe(10);
210
+ });
211
+
212
+ /*
213
+ * The name lives inside a jsonb column with no width of its own, so nothing
214
+ * downstream would ever reject an essay. It is bounded here to the same
215
+ * ShortText width the scan's own name uses, because it is rendered on one
216
+ * line in a card header and inlined into probe log lines.
217
+ */
218
+ it("bounds a config name at the ShortText width the rest of the product uses", () => {
219
+ expect(MAX_SNMP_CONFIG_NAME_LENGTH).toBe(ColumnLength.ShortText);
220
+ expect(MAX_SNMP_CONFIG_NAME_LENGTH).toBe(100);
221
+ });
222
+
223
+ /*
224
+ * These two constants are not style choices, they are the widths of two
225
+ * database columns, and they exist only to protect those columns.
226
+ *
227
+ * `snmpConfigs` is jsonb and has no width at all, so on its own it would
228
+ * store a novel in a community string quite happily. But
229
+ * getMirroredLegacyColumns copies the FIRST config onto the flattened
230
+ * snmpCommunityString / snmpV3* columns that every probe older than this
231
+ * feature still reads, and those are varchar(100) and varchar(500).
232
+ * Postgres does not truncate an over-long value on the way in — it throws.
233
+ *
234
+ * So if either constant ever drifts away from the column it mirrors into,
235
+ * validation starts accepting values the write cannot store, and the save
236
+ * fails at the database with an error naming a column the operator can no
237
+ * longer see on the form. Asserted against ColumnLength rather than against
238
+ * a literal alone, so moving the column moves the bound with it.
239
+ */
240
+ it("bounds the short credential fields at the ShortText width of the columns they are mirrored into", () => {
241
+ expect(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH).toBe(ColumnLength.ShortText);
242
+ expect(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH).toBe(100);
243
+ });
244
+
245
+ /*
246
+ * The two v3 keys get the wider bound because the columns they mirror into
247
+ * are wider. Pinned as a strict inequality as well as a value, because the
248
+ * cheap way to write this rule — one bound for every credential field — is
249
+ * exactly the mistake that would silently refuse a legitimate 200-character
250
+ * privacy passphrase.
251
+ */
252
+ it("bounds the two v3 keys at the LongText width of the columns they are mirrored into", () => {
253
+ expect(MAX_SNMP_CONFIG_KEY_LENGTH).toBe(ColumnLength.LongText);
254
+ expect(MAX_SNMP_CONFIG_KEY_LENGTH).toBe(500);
255
+ expect(MAX_SNMP_CONFIG_KEY_LENGTH).toBeGreaterThan(
256
+ MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH,
257
+ );
258
+ });
259
+
260
+ /*
261
+ * MAX_SNMP_CONFIGS_PER_SCAN is justified in prose by arithmetic over
262
+ * another module's constant — how long a full-range sweep takes per
263
+ * credential set, and therefore how many sets fit inside the probe's
264
+ * deadline. That prose once did its arithmetic against a 4,096-address
265
+ * ceiling that had already moved to 32,768, which made it claim a
266
+ * single-set full-range sweep cost a few minutes when it really costs
267
+ * something like thirty-four of them, and made the ceiling read as though
268
+ * it had been chosen to make the worst case fit. It cannot be: at this
269
+ * scan size THREE sets already cross the probe's 90-minute deadline, and a
270
+ * single-credential sweep of a maximum target has always sat within a
271
+ * factor of three of it.
272
+ *
273
+ * A comment that computes against a constant in another file goes stale in
274
+ * silence, because nothing links the two. This test is that link: it pins
275
+ * the scan ceiling the rationale rests on, and the numbers the rationale
276
+ * quotes, so moving MAX_SCAN_HOSTS again fails here instead of quietly
277
+ * turning the explanation into fiction.
278
+ */
279
+ it("keeps the sweep-cost rationale for the config ceiling tied to the real scan-size ceiling", () => {
280
+ expect(ScanTargetUtil.MAX_SCAN_HOSTS).toBe(32768);
281
+
282
+ /*
283
+ * The comment's own formula: hosts x sets x 2s SNMP timeout / 32
284
+ * concurrent probes, expressed in minutes for one set.
285
+ */
286
+ const minutesPerConfig: number =
287
+ (ScanTargetUtil.MAX_SCAN_HOSTS * 2) / 32 / 60;
288
+
289
+ expect(Math.round(minutesPerConfig)).toBe(34);
290
+
291
+ // Two sets fit inside the probe's 90-minute deadline. Three do not.
292
+ expect(minutesPerConfig * 2).toBeLessThan(90);
293
+ expect(minutesPerConfig * 3).toBeGreaterThan(90);
294
+
295
+ // And the ceiling itself is the "some 5.7 hours" the comment quotes.
296
+ const hoursAtTheCeiling: number =
297
+ (minutesPerConfig * MAX_SNMP_CONFIGS_PER_SCAN) / 60;
298
+
299
+ expect(Math.round(hoursAtTheCeiling * 10) / 10).toBe(5.7);
300
+ });
301
+
302
+ it("bounds the port to the UDP port range", () => {
303
+ expect(MINIMUM_SNMP_PORT).toBe(1);
304
+ expect(MAXIMUM_SNMP_PORT).toBe(65535);
305
+ });
306
+ });
307
+
308
+ describe("SnmpScanConfigUtil.resolve", () => {
309
+ /*
310
+ * The single most important property of this function: the probe sweeps
311
+ * with what it returns, so an empty answer is a scan that tries nothing and
312
+ * reports a confident zero. There is no input that produces one.
313
+ */
314
+ it("never returns an empty list, whatever the column holds", () => {
315
+ const columnValues: Array<unknown> = [
316
+ undefined,
317
+ null,
318
+ [],
319
+ "not-a-list",
320
+ 42,
321
+ true,
322
+ { id: "an-object-not-a-list" },
323
+ [null, undefined, "junk", 7, false, ""],
324
+ ];
325
+
326
+ for (const value of columnValues) {
327
+ const configs: Array<DiscoveryScanSnmpConfig> =
328
+ SnmpScanConfigUtil.resolve(scanWithConfigs(value));
329
+
330
+ expect(configs.length).toBeGreaterThan(0);
331
+ }
332
+ });
333
+
334
+ describe("when the scan has no usable stored list", () => {
335
+ /*
336
+ * The legacy shape is synthesized on every read rather than migrated into
337
+ * the new column: a data migration over every historical scan buys
338
+ * nothing (this produces the identical config each time) and would have
339
+ * to guess at rows whose columns are half populated.
340
+ */
341
+ it("synthesizes exactly one config from the flattened columns, with the legacy id", () => {
342
+ const configs: Array<DiscoveryScanSnmpConfig> =
343
+ SnmpScanConfigUtil.resolve(LEGACY_SCAN);
344
+
345
+ expect(configs).toHaveLength(1);
346
+ expect(configs[0]).toEqual({
347
+ id: LEGACY_SNMP_CONFIG_ID,
348
+ snmpVersion: "V2c",
349
+ snmpCommunityString: "legacy-community",
350
+ snmpPort: 1161,
351
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
352
+ snmpV3Username: "legacy-user",
353
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA,
354
+ snmpV3AuthKey: "legacy-auth-key",
355
+ snmpV3PrivProtocol: SnmpPrivProtocol.AES,
356
+ snmpV3PrivKey: "legacy-priv-key",
357
+ });
358
+ expect(configs[0]!.id).toBe(LEGACY_SNMP_CONFIG_ID);
359
+ });
360
+
361
+ /*
362
+ * Same config for null, undefined, an empty array, a value that is not an
363
+ * array at all, and an array holding nothing usable. Every one of those
364
+ * states means "this row is described by its flattened columns", and the
365
+ * probe must not behave differently for any of them.
366
+ */
367
+ it("falls back to the flattened columns for null, undefined, an empty list, a non-list and a list of junk", () => {
368
+ const columnValues: Array<unknown> = [
369
+ undefined,
370
+ null,
371
+ [],
372
+ "",
373
+ "[]",
374
+ 0,
375
+ { snmpCommunityString: "an object is not a list" },
376
+ [null, undefined, "junk", 7, false, "", 0],
377
+ ];
378
+
379
+ for (const value of columnValues) {
380
+ const configs: Array<DiscoveryScanSnmpConfig> =
381
+ SnmpScanConfigUtil.resolve({
382
+ ...LEGACY_SCAN,
383
+ snmpConfigs: value as
384
+ | Array<DiscoveryScanSnmpConfig>
385
+ | null
386
+ | undefined,
387
+ });
388
+
389
+ expect(configs).toHaveLength(1);
390
+ expect(configs[0]!.id).toBe(LEGACY_SNMP_CONFIG_ID);
391
+ expect(configs[0]!.snmpCommunityString).toBe("legacy-community");
392
+ }
393
+ });
394
+
395
+ /*
396
+ * A scan with no SNMP columns set at all is still a scan the probe has to
397
+ * sweep with something: an id, and the version default the column itself
398
+ * carries. Nothing is invented for the credentials — the probe falls back
399
+ * to "public" and port 161 on its own.
400
+ */
401
+ it("still produces one usable config for a scan with no SNMP columns set", () => {
402
+ const configs: Array<DiscoveryScanSnmpConfig> =
403
+ SnmpScanConfigUtil.resolve({});
404
+
405
+ expect(configs).toEqual([
406
+ {
407
+ id: LEGACY_SNMP_CONFIG_ID,
408
+ snmpVersion: "V2c",
409
+ },
410
+ ]);
411
+ });
412
+
413
+ /*
414
+ * Resolving the same row twice has to produce the same ids, because it
415
+ * happens independently in the probe (which stamps the id onto a host)
416
+ * and in the importer (which looks it up again).
417
+ */
418
+ it("is deterministic, so a stamped config id resolves to the same config in another process", () => {
419
+ expect(SnmpScanConfigUtil.resolve(LEGACY_SCAN)).toEqual(
420
+ SnmpScanConfigUtil.resolve(LEGACY_SCAN),
421
+ );
422
+ });
423
+ });
424
+
425
+ describe("when the scan has a stored list", () => {
426
+ it("returns the stored list rather than the flattened columns", () => {
427
+ const configs: Array<DiscoveryScanSnmpConfig> =
428
+ SnmpScanConfigUtil.resolve({
429
+ ...LEGACY_SCAN,
430
+ snmpConfigs: [
431
+ {
432
+ id: "stored-1",
433
+ name: "Access switches",
434
+ snmpVersion: "V2c",
435
+ snmpCommunityString: "stored-community",
436
+ },
437
+ ],
438
+ });
439
+
440
+ expect(configs).toHaveLength(1);
441
+ expect(configs[0]!.id).toBe("stored-1");
442
+ expect(configs[0]!.snmpCommunityString).toBe("stored-community");
443
+ // The flattened columns are ignored entirely once a list exists.
444
+ expect(configs[0]!.snmpV3Username).toBeUndefined();
445
+ });
446
+
447
+ /*
448
+ * The list is ordered by the operator's own preference and the sweep
449
+ * consumes it in that order (adaptively re-ordered only within a single
450
+ * run), so reordering it here would change which credential set a host
451
+ * answers first.
452
+ */
453
+ it("preserves the operator's order exactly", () => {
454
+ const configs: Array<DiscoveryScanSnmpConfig> =
455
+ SnmpScanConfigUtil.resolve(
456
+ scanWithConfigs([
457
+ { id: "c", name: "Printers" },
458
+ { id: "a", name: "Core" },
459
+ { id: "b", name: "Access" },
460
+ ]),
461
+ );
462
+
463
+ expect(
464
+ configs.map((config: DiscoveryScanSnmpConfig): string | undefined => {
465
+ return config.name;
466
+ }),
467
+ ).toEqual(["Printers", "Core", "Access"]);
468
+ expect(
469
+ configs.map((config: DiscoveryScanSnmpConfig): string | undefined => {
470
+ return config.id;
471
+ }),
472
+ ).toEqual(["c", "a", "b"]);
473
+ });
474
+
475
+ /*
476
+ * A jsonb column can hold anything an out-of-band writer put there. Junk
477
+ * entries are dropped rather than turned into empty credential sets,
478
+ * which would otherwise cost the sweep a full timeout per host for a
479
+ * config nobody configured.
480
+ */
481
+ it("drops entries that are not objects and keeps the ones that are", () => {
482
+ const configs: Array<DiscoveryScanSnmpConfig> =
483
+ SnmpScanConfigUtil.resolve(
484
+ scanWithConfigs([
485
+ null,
486
+ { id: "real-1", name: "Core" },
487
+ "junk",
488
+ 42,
489
+ undefined,
490
+ false,
491
+ { id: "real-2", name: "Access" },
492
+ ]),
493
+ );
494
+
495
+ expect(configs).toHaveLength(2);
496
+ expect(
497
+ configs.map((config: DiscoveryScanSnmpConfig): string | undefined => {
498
+ return config.id;
499
+ }),
500
+ ).toEqual(["real-1", "real-2"]);
501
+ });
502
+
503
+ /*
504
+ * The positional id is a REPAIR for a row written out of band, not the
505
+ * normal path — the form mints an id per card and the write hooks mint
506
+ * one for anything that arrives without. It is index-derived rather than
507
+ * random so that two independent resolves of the same row agree.
508
+ */
509
+ it("assigns index-derived ids only to entries that have none, leaving supplied ids alone", () => {
510
+ const configs: Array<DiscoveryScanSnmpConfig> =
511
+ SnmpScanConfigUtil.resolve(
512
+ scanWithConfigs([
513
+ { name: "First" },
514
+ { id: "operator-chosen-id", name: "Second" },
515
+ { name: "Third" },
516
+ ]),
517
+ );
518
+
519
+ expect(
520
+ configs.map((config: DiscoveryScanSnmpConfig): string | undefined => {
521
+ return config.id;
522
+ }),
523
+ ).toEqual(["config-1", "operator-chosen-id", "config-3"]);
524
+ });
525
+
526
+ // A blank id is no id at all, so it gets the positional repair too.
527
+ it("treats a blank id as missing", () => {
528
+ const configs: Array<DiscoveryScanSnmpConfig> =
529
+ SnmpScanConfigUtil.resolve(
530
+ scanWithConfigs([
531
+ { id: " ", name: "First" },
532
+ { id: "", name: "Second" },
533
+ ]),
534
+ );
535
+
536
+ expect(configs[0]!.id).toBe("config-1");
537
+ expect(configs[1]!.id).toBe("config-2");
538
+ });
539
+
540
+ // A supplied id is trimmed, so " abc " and "abc" name the same config.
541
+ it("trims a supplied id", () => {
542
+ const configs: Array<DiscoveryScanSnmpConfig> =
543
+ SnmpScanConfigUtil.resolve(scanWithConfigs([{ id: " spaced-id " }]));
544
+
545
+ expect(configs[0]!.id).toBe("spaced-id");
546
+ });
547
+ });
548
+
549
+ describe("version normalization", () => {
550
+ /*
551
+ * Two spellings exist — the stored one ("V3") and the probe-contract enum
552
+ * value ("3") — and a hand-written row can hold either, in any case.
553
+ * Normalizing to the stored spelling is what lets the service compare a
554
+ * saved list against an incoming one without a false difference, and what
555
+ * repairs a row that would otherwise disagree with the sweep.
556
+ */
557
+ it("normalizes every spelling and casing of v3 to the stored spelling", () => {
558
+ const spellings: Array<string> = ["3", "v3", "V3", " V3 ", " v3", "3 "];
559
+
560
+ for (const spelling of spellings) {
561
+ const configs: Array<DiscoveryScanSnmpConfig> =
562
+ SnmpScanConfigUtil.resolve(
563
+ scanWithConfigs([{ id: "x", snmpVersion: spelling }]),
564
+ );
565
+
566
+ expect(configs[0]!.snmpVersion).toBe("V3");
567
+ }
568
+ });
569
+
570
+ it("normalizes every spelling and casing of v1 to the stored spelling", () => {
571
+ const spellings: Array<string> = ["1", "v1", "V1", " V1 "];
572
+
573
+ for (const spelling of spellings) {
574
+ const configs: Array<DiscoveryScanSnmpConfig> =
575
+ SnmpScanConfigUtil.resolve(
576
+ scanWithConfigs([{ id: "x", snmpVersion: spelling }]),
577
+ );
578
+
579
+ expect(configs[0]!.snmpVersion).toBe("V1");
580
+ }
581
+ });
582
+
583
+ /*
584
+ * Anything unrecognized lands on the SAME default the probe would have
585
+ * used, rather than being stored as itself. A config whose stored version
586
+ * says one thing while the sweep does another is unexplainable from the
587
+ * UI.
588
+ */
589
+ it("falls back to the stored v2c spelling for every other value, including nonsense", () => {
590
+ const spellings: Array<unknown> = [
591
+ "2c",
592
+ "V2c",
593
+ "v2C",
594
+ " 2C ",
595
+ "2",
596
+ "banana",
597
+ "",
598
+ " ",
599
+ undefined,
600
+ null,
601
+ ];
602
+
603
+ for (const spelling of spellings) {
604
+ const configs: Array<DiscoveryScanSnmpConfig> =
605
+ SnmpScanConfigUtil.resolve(
606
+ scanWithConfigs([{ id: "x", snmpVersion: spelling }]),
607
+ );
608
+
609
+ expect(configs[0]!.snmpVersion).toBe("V2c");
610
+ }
611
+ });
612
+
613
+ // The version is the one field that is always present after resolving.
614
+ it("always sets a version, even on a config that carried none", () => {
615
+ const configs: Array<DiscoveryScanSnmpConfig> =
616
+ SnmpScanConfigUtil.resolve(scanWithConfigs([{ id: "x" }]));
617
+
618
+ expect(Object.keys(configs[0]!)).toContain("snmpVersion");
619
+ expect(configs[0]!.snmpVersion).toBe("V2c");
620
+ });
621
+ });
622
+
623
+ describe("string and port cleanup", () => {
624
+ it("trims every string field", () => {
625
+ const configs: Array<DiscoveryScanSnmpConfig> =
626
+ SnmpScanConfigUtil.resolve(
627
+ scanWithConfigs([
628
+ {
629
+ id: " x ",
630
+ name: " Core switches ",
631
+ snmpCommunityString: " public ",
632
+ snmpV3SecurityLevel: " authPriv ",
633
+ snmpV3Username: " monitor ",
634
+ snmpV3AuthProtocol: " SHA256 ",
635
+ snmpV3AuthKey: " auth-key-value ",
636
+ snmpV3PrivProtocol: " AES256 ",
637
+ snmpV3PrivKey: " priv-key-value ",
638
+ },
639
+ ]),
640
+ );
641
+
642
+ expect(configs[0]).toEqual({
643
+ id: "x",
644
+ name: "Core switches",
645
+ snmpVersion: "V2c",
646
+ snmpCommunityString: "public",
647
+ snmpV3SecurityLevel: "authPriv",
648
+ snmpV3Username: "monitor",
649
+ snmpV3AuthProtocol: "SHA256",
650
+ snmpV3AuthKey: "auth-key-value",
651
+ snmpV3PrivProtocol: "AES256",
652
+ snmpV3PrivKey: "priv-key-value",
653
+ });
654
+ });
655
+
656
+ /*
657
+ * A blank field is dropped rather than stored as "". Downstream every
658
+ * reader asks `config.snmpCommunityString ?? "public"`, and an empty
659
+ * string would defeat that fallback and dial the device with no
660
+ * community at all.
661
+ */
662
+ it("drops blank strings instead of storing empty ones", () => {
663
+ const configs: Array<DiscoveryScanSnmpConfig> =
664
+ SnmpScanConfigUtil.resolve(
665
+ scanWithConfigs([
666
+ {
667
+ id: "x",
668
+ name: " ",
669
+ snmpCommunityString: "",
670
+ snmpV3Username: "\t",
671
+ snmpV3AuthKey: "\n",
672
+ },
673
+ ]),
674
+ );
675
+
676
+ expect(configs[0]).toEqual({ id: "x", snmpVersion: "V2c" });
677
+ expect(Object.keys(configs[0]!)).not.toContain("name");
678
+ expect(Object.keys(configs[0]!)).not.toContain("snmpCommunityString");
679
+ });
680
+
681
+ // Values that are not text at all are dropped, not coerced.
682
+ it("drops fields that are not strings rather than stringifying them", () => {
683
+ const configs: Array<DiscoveryScanSnmpConfig> =
684
+ SnmpScanConfigUtil.resolve(
685
+ scanWithConfigs([
686
+ {
687
+ id: "x",
688
+ name: 42,
689
+ snmpCommunityString: { nested: true },
690
+ snmpV3Username: ["monitor"],
691
+ },
692
+ ]),
693
+ );
694
+
695
+ expect(configs[0]).toEqual({ id: "x", snmpVersion: "V2c" });
696
+ });
697
+
698
+ it("keeps a valid port, including one supplied as text by a Number form field", () => {
699
+ expect(
700
+ SnmpScanConfigUtil.resolve(
701
+ scanWithConfigs([{ id: "x", snmpPort: 1161 }]),
702
+ )[0]!.snmpPort,
703
+ ).toBe(1161);
704
+ expect(
705
+ SnmpScanConfigUtil.resolve(
706
+ scanWithConfigs([{ id: "x", snmpPort: "1161" }]),
707
+ )[0]!.snmpPort,
708
+ ).toBe(1161);
709
+ expect(
710
+ SnmpScanConfigUtil.resolve(
711
+ scanWithConfigs([{ id: "x", snmpPort: MINIMUM_SNMP_PORT }]),
712
+ )[0]!.snmpPort,
713
+ ).toBe(1);
714
+ expect(
715
+ SnmpScanConfigUtil.resolve(
716
+ scanWithConfigs([{ id: "x", snmpPort: MAXIMUM_SNMP_PORT }]),
717
+ )[0]!.snmpPort,
718
+ ).toBe(65535);
719
+ });
720
+
721
+ /*
722
+ * Out of range is DROPPED, not clamped. Clamping 0 to 1 would have the
723
+ * probe dial a port the operator never chose and never sees; dropping it
724
+ * lets the column default (161) apply, which is the port they meant. The
725
+ * write path refuses the value outright via getPortValidationError — this
726
+ * is only what a READ does with a row that already holds one.
727
+ */
728
+ it("drops an out-of-range or non-integer port rather than clamping it", () => {
729
+ const badPorts: Array<unknown> = [
730
+ 0,
731
+ -1,
732
+ 65536,
733
+ 99999,
734
+ 161.5,
735
+ "161.5",
736
+ "abc",
737
+ Number.NaN,
738
+ Number.POSITIVE_INFINITY,
739
+ "",
740
+ " ",
741
+ ];
742
+
743
+ for (const port of badPorts) {
744
+ const configs: Array<DiscoveryScanSnmpConfig> =
745
+ SnmpScanConfigUtil.resolve(
746
+ scanWithConfigs([{ id: "x", snmpPort: port }]),
747
+ );
748
+
749
+ expect(configs[0]!.snmpPort).toBeUndefined();
750
+ expect(Object.keys(configs[0]!)).not.toContain("snmpPort");
751
+ }
752
+ });
753
+ });
754
+
755
+ describe("credential preservation across a version change", () => {
756
+ /*
757
+ * An operator who switches a card to v2c to try something, then switches
758
+ * it back, must not find their v3 keys gone. The probe reads only what
759
+ * the chosen version needs, so carrying the unused block costs nothing
760
+ * and losing it costs a re-typed key nobody remembers.
761
+ */
762
+ it("keeps the v3 block on a v2c config", () => {
763
+ const configs: Array<DiscoveryScanSnmpConfig> =
764
+ SnmpScanConfigUtil.resolve(
765
+ scanWithConfigs([
766
+ {
767
+ id: "x",
768
+ snmpVersion: "V2c",
769
+ snmpCommunityString: "public",
770
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
771
+ snmpV3Username: "monitor",
772
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA256,
773
+ snmpV3AuthKey: "auth-key-value",
774
+ snmpV3PrivProtocol: SnmpPrivProtocol.AES256,
775
+ snmpV3PrivKey: "priv-key-value",
776
+ },
777
+ ]),
778
+ );
779
+
780
+ expect(configs[0]!.snmpVersion).toBe("V2c");
781
+ expect(configs[0]!.snmpV3Username).toBe("monitor");
782
+ expect(configs[0]!.snmpV3AuthKey).toBe("auth-key-value");
783
+ expect(configs[0]!.snmpV3PrivKey).toBe("priv-key-value");
784
+ expect(configs[0]!.snmpV3SecurityLevel).toBe(SnmpSecurityLevel.AuthPriv);
785
+ });
786
+
787
+ it("keeps the community string on a v3 config", () => {
788
+ const configs: Array<DiscoveryScanSnmpConfig> =
789
+ SnmpScanConfigUtil.resolve(
790
+ scanWithConfigs([
791
+ {
792
+ id: "x",
793
+ snmpVersion: "V3",
794
+ snmpCommunityString: "still-here",
795
+ snmpV3Username: "monitor",
796
+ },
797
+ ]),
798
+ );
799
+
800
+ expect(configs[0]!.snmpVersion).toBe("V3");
801
+ expect(configs[0]!.snmpCommunityString).toBe("still-here");
802
+ });
803
+ });
804
+ });
805
+
806
+ describe("SnmpScanConfigUtil.readStoredList", () => {
807
+ /*
808
+ * This is the WRITER's question — "does this row carry its own list?" —
809
+ * and it has to be answered honestly, because "has a list" and "is
810
+ * described by its flattened columns" are two different states of the row
811
+ * and the write hooks branch on which one it is.
812
+ */
813
+ it("returns null for anything that is not a populated list", () => {
814
+ const values: Array<unknown> = [
815
+ undefined,
816
+ null,
817
+ [],
818
+ "",
819
+ "not-a-list",
820
+ "[]",
821
+ 0,
822
+ 42,
823
+ true,
824
+ false,
825
+ { id: "an object is not a list" },
826
+ ];
827
+
828
+ for (const value of values) {
829
+ expect(SnmpScanConfigUtil.readStoredList(value)).toBeNull();
830
+ }
831
+ });
832
+
833
+ it("returns null for a list that holds nothing usable", () => {
834
+ expect(
835
+ SnmpScanConfigUtil.readStoredList([
836
+ null,
837
+ undefined,
838
+ "junk",
839
+ 7,
840
+ false,
841
+ "",
842
+ ]),
843
+ ).toBeNull();
844
+ });
845
+
846
+ it("returns the entries that are objects, dropping the rest", () => {
847
+ const stored: Array<DiscoveryScanSnmpConfig> | null =
848
+ SnmpScanConfigUtil.readStoredList([
849
+ null,
850
+ { id: "real-1" },
851
+ "junk",
852
+ { id: "real-2" },
853
+ ]);
854
+
855
+ expect(stored).toEqual([{ id: "real-1" }, { id: "real-2" }]);
856
+ });
857
+
858
+ /*
859
+ * Deliberately NOT normalized: this is what a writer compares against and
860
+ * what it decides to overwrite, so it has to be the row as stored — the
861
+ * same objects, untrimmed, with no ids or versions invented. resolve() is
862
+ * the function that repairs; this one only reports.
863
+ */
864
+ it("hands back the stored entries as they are, without normalizing them", () => {
865
+ const first: DiscoveryScanSnmpConfig = {
866
+ name: " Core ",
867
+ snmpVersion: "3",
868
+ };
869
+ const stored: Array<DiscoveryScanSnmpConfig> | null =
870
+ SnmpScanConfigUtil.readStoredList([first]);
871
+
872
+ expect(stored).toHaveLength(1);
873
+ expect(stored![0]).toBe(first);
874
+ expect(stored![0]!.name).toBe(" Core ");
875
+ expect(stored![0]!.snmpVersion).toBe("3");
876
+ expect(stored![0]!.id).toBeUndefined();
877
+ });
878
+
879
+ /*
880
+ * The contrast that matters. Given the same row, resolve() answers "what
881
+ * does the scan try?" and is never empty; readStoredList() answers "does
882
+ * this row have its own list?" and says no when it does not. A writer that
883
+ * used resolve() would materialize a list on a row nobody asked to change.
884
+ */
885
+ it("says null exactly where resolve synthesizes a legacy config", () => {
886
+ const emptyish: Array<unknown> = [undefined, null, [], "junk", ["junk"]];
887
+
888
+ for (const value of emptyish) {
889
+ expect(SnmpScanConfigUtil.readStoredList(value)).toBeNull();
890
+
891
+ const resolved: Array<DiscoveryScanSnmpConfig> =
892
+ SnmpScanConfigUtil.resolve({
893
+ ...LEGACY_SCAN,
894
+ snmpConfigs: value as
895
+ | Array<DiscoveryScanSnmpConfig>
896
+ | null
897
+ | undefined,
898
+ });
899
+
900
+ expect(resolved).toHaveLength(1);
901
+ expect(resolved[0]!.id).toBe(LEGACY_SNMP_CONFIG_ID);
902
+ }
903
+ });
904
+
905
+ it("and returns the list exactly where resolve returns the stored one", () => {
906
+ const list: Array<DiscoveryScanSnmpConfig> = [
907
+ { id: "a", name: "Core" },
908
+ { id: "b", name: "Access" },
909
+ ];
910
+
911
+ expect(SnmpScanConfigUtil.readStoredList(list)).toHaveLength(2);
912
+ expect(SnmpScanConfigUtil.resolve(scanWithConfigs(list))).toHaveLength(2);
913
+ });
914
+ });
915
+
916
+ describe("SnmpScanConfigUtil.findById", () => {
917
+ const configs: Array<DiscoveryScanSnmpConfig> = [
918
+ { id: "config-a", name: "Core", snmpCommunityString: "community-a" },
919
+ { id: "config-b", name: "Access", snmpCommunityString: "community-b" },
920
+ ];
921
+
922
+ it("returns the config whose id matches exactly", () => {
923
+ expect(SnmpScanConfigUtil.findById(configs, "config-b")).toBe(configs[1]);
924
+ });
925
+
926
+ it("returns undefined when the id names no config in the list", () => {
927
+ expect(
928
+ SnmpScanConfigUtil.findById(configs, "config-that-was-deleted"),
929
+ ).toBeUndefined();
930
+ });
931
+
932
+ /*
933
+ * Every result stored before this feature carries no config id at all, and
934
+ * so does every ping-only host. Those are the common case, not the edge
935
+ * case — hence undefined rather than a throw.
936
+ */
937
+ it("returns undefined when there is no id to look up", () => {
938
+ expect(SnmpScanConfigUtil.findById(configs, undefined)).toBeUndefined();
939
+ expect(SnmpScanConfigUtil.findById(configs, null)).toBeUndefined();
940
+ expect(SnmpScanConfigUtil.findById(configs, "")).toBeUndefined();
941
+ });
942
+
943
+ /*
944
+ * Ids are compared verbatim: they are minted UUIDs, and being lenient here
945
+ * would let two configs that differ only by case both answer for a host.
946
+ */
947
+ it("matches ids exactly, without trimming or case-folding", () => {
948
+ expect(SnmpScanConfigUtil.findById(configs, "CONFIG-A")).toBeUndefined();
949
+ expect(SnmpScanConfigUtil.findById(configs, " config-a ")).toBeUndefined();
950
+ });
951
+
952
+ it("returns undefined against an empty list", () => {
953
+ expect(SnmpScanConfigUtil.findById([], "config-a")).toBeUndefined();
954
+ });
955
+ });
956
+
957
+ describe("SnmpScanConfigUtil.resolveForHost", () => {
958
+ const MULTI_CONFIG_SCAN: SnmpScanConfigSource = {
959
+ ...LEGACY_SCAN,
960
+ snmpConfigs: [
961
+ { id: "first", name: "Access", snmpCommunityString: "community-first" },
962
+ { id: "second", name: "Core", snmpCommunityString: "community-second" },
963
+ { id: "third", name: "Printers", snmpCommunityString: "community-third" },
964
+ ],
965
+ };
966
+
967
+ /*
968
+ * The whole point of stamping a config id onto a discovered host: the
969
+ * device is imported with THE credential set that actually answered it,
970
+ * not with the first one in the list.
971
+ */
972
+ it("returns the config that answered the host", () => {
973
+ const config: DiscoveryScanSnmpConfig = SnmpScanConfigUtil.resolveForHost(
974
+ MULTI_CONFIG_SCAN,
975
+ "third",
976
+ );
977
+
978
+ expect(config.id).toBe("third");
979
+ expect(config.snmpCommunityString).toBe("community-third");
980
+ });
981
+
982
+ /*
983
+ * A device imported with the wrong credentials is repairable on the device
984
+ * form; a device imported with NONE can never poll and gives the operator
985
+ * nothing to correct. So the fallback is the first config, never nothing.
986
+ */
987
+ it("falls back to the first config when the host carries no config id", () => {
988
+ for (const hostConfigId of [undefined, null, ""]) {
989
+ const config: DiscoveryScanSnmpConfig = SnmpScanConfigUtil.resolveForHost(
990
+ MULTI_CONFIG_SCAN,
991
+ hostConfigId,
992
+ );
993
+
994
+ expect(config.id).toBe("first");
995
+ expect(config.snmpCommunityString).toBe("community-first");
996
+ }
997
+ });
998
+
999
+ // The operator deleted the card that found this host between scan and import.
1000
+ it("falls back to the first config when the stamped id no longer exists", () => {
1001
+ const config: DiscoveryScanSnmpConfig = SnmpScanConfigUtil.resolveForHost(
1002
+ MULTI_CONFIG_SCAN,
1003
+ "a-config-that-was-deleted",
1004
+ );
1005
+
1006
+ expect(config.id).toBe("first");
1007
+ expect(config.snmpCommunityString).toBe("community-first");
1008
+ });
1009
+
1010
+ /*
1011
+ * The compatibility guarantee, stated as a test: for a scan configured the
1012
+ * old way the first (and only) config IS the flattened columns, so every
1013
+ * result stored before this feature imports with exactly the credentials it
1014
+ * would have imported with before.
1015
+ */
1016
+ it("resolves a legacy scan's flattened columns for a host with no config id", () => {
1017
+ const config: DiscoveryScanSnmpConfig = SnmpScanConfigUtil.resolveForHost(
1018
+ LEGACY_SCAN,
1019
+ undefined,
1020
+ );
1021
+
1022
+ expect(config.id).toBe(LEGACY_SNMP_CONFIG_ID);
1023
+ expect(config.snmpCommunityString).toBe("legacy-community");
1024
+ expect(config.snmpPort).toBe(1161);
1025
+ expect(config.snmpV3Username).toBe("legacy-user");
1026
+ expect(config.snmpV3AuthKey).toBe("legacy-auth-key");
1027
+ expect(config.snmpV3PrivKey).toBe("legacy-priv-key");
1028
+ });
1029
+
1030
+ it("resolves a legacy scan's flattened columns for a host stamped with an unknown id", () => {
1031
+ const config: DiscoveryScanSnmpConfig = SnmpScanConfigUtil.resolveForHost(
1032
+ LEGACY_SCAN,
1033
+ "some-id-from-another-scan",
1034
+ );
1035
+
1036
+ expect(config.id).toBe(LEGACY_SNMP_CONFIG_ID);
1037
+ expect(config.snmpCommunityString).toBe("legacy-community");
1038
+ });
1039
+
1040
+ // The literal id the probe stamps for a legacy scan resolves back to it.
1041
+ it("resolves the legacy id itself back to the flattened columns", () => {
1042
+ const config: DiscoveryScanSnmpConfig = SnmpScanConfigUtil.resolveForHost(
1043
+ LEGACY_SCAN,
1044
+ LEGACY_SNMP_CONFIG_ID,
1045
+ );
1046
+
1047
+ expect(config.snmpCommunityString).toBe("legacy-community");
1048
+ });
1049
+
1050
+ // Never undefined, because resolve() is never empty.
1051
+ it("always returns a config, even for a scan with no SNMP columns at all", () => {
1052
+ const config: DiscoveryScanSnmpConfig = SnmpScanConfigUtil.resolveForHost(
1053
+ {},
1054
+ "anything",
1055
+ );
1056
+
1057
+ expect(config).toBeDefined();
1058
+ expect(config.id).toBe(LEGACY_SNMP_CONFIG_ID);
1059
+ });
1060
+ });
1061
+
1062
+ describe("SnmpScanConfigUtil.toStoredVersion", () => {
1063
+ it("maps every recognized v1 spelling to the stored one", () => {
1064
+ for (const value of ["1", "v1", "V1", " V1 "]) {
1065
+ expect(SnmpScanConfigUtil.toStoredVersion(value)).toBe("V1");
1066
+ }
1067
+ });
1068
+
1069
+ it("maps every recognized v3 spelling to the stored one", () => {
1070
+ for (const value of ["3", "v3", "V3", " v3 "]) {
1071
+ expect(SnmpScanConfigUtil.toStoredVersion(value)).toBe("V3");
1072
+ }
1073
+ });
1074
+
1075
+ it("maps every recognized v2c spelling to the stored one", () => {
1076
+ for (const value of ["2c", "V2c", "v2C", " 2c "]) {
1077
+ expect(SnmpScanConfigUtil.toStoredVersion(value)).toBe("V2c");
1078
+ }
1079
+ });
1080
+
1081
+ /*
1082
+ * Written as a map off the PARSED enum rather than by upper-casing the
1083
+ * input, so an unrecognized value lands on the same default the probe would
1084
+ * have used instead of being stored as itself and disagreeing with the
1085
+ * sweep.
1086
+ */
1087
+ it("falls back to v2c for anything unrecognized, absent or blank", () => {
1088
+ for (const value of ["", " ", "2", "banana", "v4", undefined, null]) {
1089
+ expect(SnmpScanConfigUtil.toStoredVersion(value)).toBe("V2c");
1090
+ }
1091
+ });
1092
+
1093
+ it("only ever returns one of the three stored spellings", () => {
1094
+ const inputs: Array<string | null | undefined> = [
1095
+ "1",
1096
+ "2c",
1097
+ "3",
1098
+ "V1",
1099
+ "V2c",
1100
+ "V3",
1101
+ "nonsense",
1102
+ "",
1103
+ null,
1104
+ undefined,
1105
+ ];
1106
+
1107
+ for (const value of inputs) {
1108
+ expect(["V1", "V2c", "V3"]).toContain(
1109
+ SnmpScanConfigUtil.toStoredVersion(value),
1110
+ );
1111
+ }
1112
+ });
1113
+ });
1114
+
1115
+ describe("SnmpScanConfigUtil.getConfigLabel", () => {
1116
+ it("uses the operator's name and the version when the config is named", () => {
1117
+ expect(
1118
+ SnmpScanConfigUtil.getConfigLabel({
1119
+ name: "Core switches",
1120
+ snmpVersion: "V3",
1121
+ }),
1122
+ ).toBe("Core switches (V3)");
1123
+ });
1124
+
1125
+ // The name wins over the position, which is what makes a five-card list readable.
1126
+ it("prefers the name even when a position was supplied", () => {
1127
+ expect(
1128
+ SnmpScanConfigUtil.getConfigLabel(
1129
+ { name: "Printers", snmpVersion: "V1" },
1130
+ 4,
1131
+ ),
1132
+ ).toBe("Printers (V1)");
1133
+ });
1134
+
1135
+ it("names an unnamed config by its one-based position", () => {
1136
+ expect(SnmpScanConfigUtil.getConfigLabel({}, 0)).toBe(
1137
+ "SNMP config 1 (V2c)",
1138
+ );
1139
+ expect(SnmpScanConfigUtil.getConfigLabel({ snmpVersion: "V3" }, 1)).toBe(
1140
+ "SNMP config 2 (V3)",
1141
+ );
1142
+ });
1143
+
1144
+ /*
1145
+ * Without a position there is nothing to number, so the label is the
1146
+ * version alone — the shape used where only one config is under discussion
1147
+ * (buildSnmpV3Auth's "configured for <config label>" message).
1148
+ */
1149
+ it("falls back to the version alone when there is neither a name nor a position", () => {
1150
+ expect(SnmpScanConfigUtil.getConfigLabel({})).toBe("V2c");
1151
+ expect(SnmpScanConfigUtil.getConfigLabel({ snmpVersion: "3" })).toBe("V3");
1152
+ });
1153
+
1154
+ it("treats a blank name as no name", () => {
1155
+ expect(SnmpScanConfigUtil.getConfigLabel({ name: " " }, 2)).toBe(
1156
+ "SNMP config 3 (V2c)",
1157
+ );
1158
+ expect(SnmpScanConfigUtil.getConfigLabel({ name: "" })).toBe("V2c");
1159
+ });
1160
+
1161
+ it("trims the name and normalizes the version it prints", () => {
1162
+ expect(
1163
+ SnmpScanConfigUtil.getConfigLabel({
1164
+ name: " Core switches ",
1165
+ snmpVersion: "3",
1166
+ }),
1167
+ ).toBe("Core switches (V3)");
1168
+ });
1169
+
1170
+ /*
1171
+ * SECURITY PROPERTY, not cosmetics. These labels are written into the
1172
+ * probe's log lines and into the scan's statusMessage, which a Viewer can
1173
+ * read — a column the credentials themselves are deliberately kept out of
1174
+ * (the snmpConfigs column's read access control is the narrow list for
1175
+ * exactly this reason). So the label is built from the operator's own name
1176
+ * and the version, and from nothing else.
1177
+ */
1178
+ it("never leaks the community string or either v3 key, whatever is set", () => {
1179
+ const community: string = "s3cr3t-community";
1180
+ const authKey: string = "s3cr3t-auth-key";
1181
+ const privKey: string = "s3cr3t-priv-key";
1182
+
1183
+ const config: DiscoveryScanSnmpConfig = {
1184
+ id: "config-id",
1185
+ name: "Core switches",
1186
+ snmpVersion: "V3",
1187
+ snmpCommunityString: community,
1188
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
1189
+ snmpV3Username: "monitor",
1190
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA512,
1191
+ snmpV3AuthKey: authKey,
1192
+ snmpV3PrivProtocol: SnmpPrivProtocol.AES256,
1193
+ snmpV3PrivKey: privKey,
1194
+ };
1195
+
1196
+ const labels: Array<string> = [
1197
+ SnmpScanConfigUtil.getConfigLabel(config),
1198
+ SnmpScanConfigUtil.getConfigLabel(config, 0),
1199
+ SnmpScanConfigUtil.getConfigLabel(config, 7),
1200
+ SnmpScanConfigUtil.getConfigLabel({ ...config, name: undefined }),
1201
+ SnmpScanConfigUtil.getConfigLabel({ ...config, name: undefined }, 2),
1202
+ ];
1203
+
1204
+ for (const label of labels) {
1205
+ expect(label).not.toContain(community);
1206
+ expect(label).not.toContain(authKey);
1207
+ expect(label).not.toContain(privKey);
1208
+ // Not even a fragment of one.
1209
+ expect(label).not.toContain("s3cr3t");
1210
+ }
1211
+
1212
+ expect(labels[0]).toBe("Core switches (V3)");
1213
+ expect(labels[4]).toBe("SNMP config 3 (V3)");
1214
+ });
1215
+ });
1216
+
1217
+ describe("SnmpScanConfigUtil.getValidationError", () => {
1218
+ /*
1219
+ * The column is optional: a scan with no list is a scan configured through
1220
+ * the flattened columns, which is every scan that existed before this
1221
+ * feature. Absence must not be an error anywhere.
1222
+ */
1223
+ it("accepts an absent list, because the column is optional", () => {
1224
+ expect(SnmpScanConfigUtil.getValidationError(undefined)).toBeNull();
1225
+ expect(SnmpScanConfigUtil.getValidationError(null)).toBeNull();
1226
+ });
1227
+
1228
+ /*
1229
+ * Typed unknown because the server hook runs before the model's own type
1230
+ * checks, so this is the first thing to see whatever the client actually
1231
+ * sent — and for a jsonb column that really can be a string or a number.
1232
+ */
1233
+ it("refuses a value that is not a list at all", () => {
1234
+ for (const value of ["", "not-a-list", "[]", 0, 42, true, { id: "x" }]) {
1235
+ expect(SnmpScanConfigUtil.getValidationError(value)).toBe(
1236
+ "SNMP configs must be a list.",
1237
+ );
1238
+ }
1239
+ });
1240
+
1241
+ /*
1242
+ * An empty list is refused rather than quietly read as "use the flattened
1243
+ * columns". The operator got here by deleting every card, and silently
1244
+ * falling back to a hidden credential set they can no longer see produces a
1245
+ * scan nobody can explain.
1246
+ */
1247
+ it("refuses an empty list rather than silently falling back to the flattened columns", () => {
1248
+ const error: string | null = SnmpScanConfigUtil.getValidationError([]);
1249
+
1250
+ expect(error).toBe(
1251
+ "Add at least one SNMP config, or the scan has no credentials to try.",
1252
+ );
1253
+ });
1254
+
1255
+ it("accepts a list of exactly the maximum length", () => {
1256
+ const configs: Array<DiscoveryScanSnmpConfig> = Array.from(
1257
+ { length: MAX_SNMP_CONFIGS_PER_SCAN },
1258
+ (_unused: unknown, index: number): DiscoveryScanSnmpConfig => {
1259
+ return { id: `config-${index}`, name: `Config ${index}` };
1260
+ },
1261
+ );
1262
+
1263
+ expect(configs).toHaveLength(10);
1264
+ expect(SnmpScanConfigUtil.getValidationError(configs)).toBeNull();
1265
+ });
1266
+
1267
+ /*
1268
+ * One over the ceiling, and the message says WHY the ceiling exists —
1269
+ * every extra config is another SNMP timeout on each silent address, and a
1270
+ * long list can push a large sweep past the probe's deadline.
1271
+ */
1272
+ it("refuses one more than the maximum and explains the cost", () => {
1273
+ const configs: Array<DiscoveryScanSnmpConfig> = Array.from(
1274
+ { length: MAX_SNMP_CONFIGS_PER_SCAN + 1 },
1275
+ (_unused: unknown, index: number): DiscoveryScanSnmpConfig => {
1276
+ return { id: `config-${index}` };
1277
+ },
1278
+ );
1279
+
1280
+ const error: string | null = SnmpScanConfigUtil.getValidationError(configs);
1281
+
1282
+ expect(error).toContain(
1283
+ `at most ${MAX_SNMP_CONFIGS_PER_SCAN} SNMP configs`,
1284
+ );
1285
+ expect(error).toContain("This one has 11.");
1286
+ expect(error).toContain("Split the range into more scans instead.");
1287
+ });
1288
+
1289
+ describe("entry shape", () => {
1290
+ /*
1291
+ * An entry that is not an object cannot carry credentials, and the
1292
+ * message names the card so the operator knows which one to delete.
1293
+ */
1294
+ it("refuses an entry that is not an object, naming its position", () => {
1295
+ for (const entry of [null, undefined, "junk", 42, true, false, 0, ""]) {
1296
+ expect(errorForSecondConfig(entry)).toBe("SNMP config 2 is not valid.");
1297
+ }
1298
+ });
1299
+
1300
+ // An array is an object to `typeof`, so it gets its own rejection here.
1301
+ it("refuses an entry that is an array", () => {
1302
+ expect(errorForSecondConfig([])).toBe("SNMP config 2 is not valid.");
1303
+ expect(errorForSecondConfig([{ id: "nested" }])).toBe(
1304
+ "SNMP config 2 is not valid.",
1305
+ );
1306
+ });
1307
+
1308
+ it("names the FIRST offending config when several are wrong", () => {
1309
+ const error: string | null = SnmpScanConfigUtil.getValidationError([
1310
+ { id: "ok" },
1311
+ { id: "bad-name", name: 42 },
1312
+ { id: "bad-port", snmpPort: 0 },
1313
+ ]);
1314
+
1315
+ expect(error).toContain("SNMP config 2");
1316
+ expect(error).not.toContain("SNMP config 3");
1317
+ });
1318
+ });
1319
+
1320
+ describe("field types", () => {
1321
+ /*
1322
+ * Every text field is checked, and each has its own human label, because
1323
+ * "SNMP config 2 is not valid" would not tell an operator which box to
1324
+ * look at.
1325
+ */
1326
+ it("refuses a non-string in any text field, naming the field and the position", () => {
1327
+ const cases: Array<[string, string]> = [
1328
+ ["id", "id"],
1329
+ ["name", "name"],
1330
+ ["snmpVersion", "version"],
1331
+ ["snmpCommunityString", "community string"],
1332
+ ["snmpV3SecurityLevel", "v3 security level"],
1333
+ ["snmpV3Username", "v3 username"],
1334
+ ["snmpV3AuthProtocol", "v3 authentication protocol"],
1335
+ ["snmpV3AuthKey", "v3 authentication key"],
1336
+ ["snmpV3PrivProtocol", "v3 privacy protocol"],
1337
+ ["snmpV3PrivKey", "v3 privacy key"],
1338
+ ];
1339
+
1340
+ for (const [key, label] of cases) {
1341
+ expect(errorForSecondConfig({ [key]: 42 })).toBe(
1342
+ `SNMP config 2: the ${label} must be text.`,
1343
+ );
1344
+ expect(errorForSecondConfig({ [key]: { nested: true } })).toBe(
1345
+ `SNMP config 2: the ${label} must be text.`,
1346
+ );
1347
+ }
1348
+ });
1349
+
1350
+ /*
1351
+ * Unset is not the same as wrong. Every one of these fields is optional,
1352
+ * and a null arriving from a cleared form field is an absence.
1353
+ */
1354
+ it("accepts null and undefined in every text field", () => {
1355
+ expect(
1356
+ SnmpScanConfigUtil.getValidationError([
1357
+ {
1358
+ id: null,
1359
+ name: null,
1360
+ snmpVersion: null,
1361
+ snmpCommunityString: null,
1362
+ snmpV3SecurityLevel: null,
1363
+ snmpV3Username: undefined,
1364
+ snmpV3AuthProtocol: undefined,
1365
+ snmpV3AuthKey: undefined,
1366
+ snmpV3PrivProtocol: undefined,
1367
+ snmpV3PrivKey: undefined,
1368
+ },
1369
+ ]),
1370
+ ).toBeNull();
1371
+ });
1372
+
1373
+ it("accepts a config with nothing set at all", () => {
1374
+ expect(SnmpScanConfigUtil.getValidationError([{}])).toBeNull();
1375
+ });
1376
+ });
1377
+
1378
+ describe("name length", () => {
1379
+ it("accepts a name of exactly the maximum length", () => {
1380
+ const name: string = "n".repeat(MAX_SNMP_CONFIG_NAME_LENGTH);
1381
+
1382
+ expect(name).toHaveLength(100);
1383
+ expect(
1384
+ SnmpScanConfigUtil.getValidationError([{ id: "x", name }]),
1385
+ ).toBeNull();
1386
+ });
1387
+
1388
+ /*
1389
+ * The value lives in a jsonb column with no width of its own, so nothing
1390
+ * downstream would ever reject an over-long name — which is exactly why
1391
+ * the message has to quote both the ceiling and what they typed.
1392
+ */
1393
+ it("refuses a name one character over the maximum and quotes both lengths", () => {
1394
+ const name: string = "n".repeat(MAX_SNMP_CONFIG_NAME_LENGTH + 1);
1395
+ const error: string | null = errorForSecondConfig({
1396
+ id: "second",
1397
+ name,
1398
+ });
1399
+
1400
+ expect(error).toContain("SNMP config 2");
1401
+ expect(error).toContain(
1402
+ `a name cannot be longer than ${MAX_SNMP_CONFIG_NAME_LENGTH} characters`,
1403
+ );
1404
+ expect(error).toContain("This one is 101.");
1405
+ });
1406
+
1407
+ // The length is measured after trimming, so padding is not what fails.
1408
+ it("measures the name after trimming", () => {
1409
+ const name: string = ` ${"n".repeat(MAX_SNMP_CONFIG_NAME_LENGTH)} `;
1410
+
1411
+ expect(
1412
+ SnmpScanConfigUtil.getValidationError([{ id: "x", name }]),
1413
+ ).toBeNull();
1414
+ });
1415
+ });
1416
+
1417
+ /*
1418
+ * CREDENTIAL FIELD LENGTHS
1419
+ *
1420
+ * `snmpConfigs` is a jsonb column and jsonb has no width, so on the storage
1421
+ * side nothing objects to a novel typed into a community string. The bound
1422
+ * comes from somewhere else entirely: getMirroredLegacyColumns copies the
1423
+ * FIRST config of the list onto the flattened snmpCommunityString /
1424
+ * snmpV3* columns, because a probe is deployed separately from the server
1425
+ * and a probe that has never heard of `snmpConfigs` reads those columns and
1426
+ * nothing else. They are varchar(100) and varchar(500), and Postgres does
1427
+ * not truncate an over-long value on the way in — it raises.
1428
+ *
1429
+ * Before these bounds existed the defect had two halves and both were bad:
1430
+ *
1431
+ * - An over-long community string in card #1 failed the WRITE, and the
1432
+ * error the operator got back named `snmpCommunityString` — a column
1433
+ * that no longer appears anywhere on the multi-config form. They were
1434
+ * told to fix a field they cannot see.
1435
+ * - The IDENTICAL value in card #2 saved without a word, and became a
1436
+ * credential the mirror could never carry. Reordering the cards, or
1437
+ * deleting card #1, then turned a save that had always worked into the
1438
+ * database error above — arbitrarily far from the edit that caused it.
1439
+ *
1440
+ * So the rule below is one rule, checked on EVERY card, phrased in the
1441
+ * words of the field the operator is looking at, and each field gets the
1442
+ * width of the column it is actually mirrored into.
1443
+ */
1444
+ describe("credential field lengths", () => {
1445
+ it("accepts every short credential field at exactly the ShortText bound", () => {
1446
+ const value: string = "s".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH);
1447
+
1448
+ expect(value).toHaveLength(100);
1449
+
1450
+ for (const [key] of SHORT_TEXT_CREDENTIAL_FIELDS) {
1451
+ expect(
1452
+ SnmpScanConfigUtil.getValidationError([configWithField(key, value)]),
1453
+ ).toBeNull();
1454
+ }
1455
+ });
1456
+
1457
+ /*
1458
+ * One character over, for every field, with the message naming the card
1459
+ * by position and quoting BOTH lengths. Position matters because a list
1460
+ * of five credential sets makes "the community string is too long" an
1461
+ * unactionable sentence; both lengths matter because the operator pasted
1462
+ * a value out of a password manager and has no idea how long it is.
1463
+ */
1464
+ it("refuses every short credential field one character over the bound, naming the card and quoting both lengths", () => {
1465
+ const value: string = "s".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH + 1);
1466
+
1467
+ expect(value).toHaveLength(101);
1468
+
1469
+ for (const [key, label] of SHORT_TEXT_CREDENTIAL_FIELDS) {
1470
+ expect(errorForSecondConfig(configWithField(key, value))).toBe(
1471
+ `SNMP config 2: the ${label} cannot be longer than ` +
1472
+ `${MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH} characters. This one is 101.`,
1473
+ );
1474
+ }
1475
+ });
1476
+
1477
+ /*
1478
+ * A 400-character key is an ordinary SNMP v3 passphrase, not an abuse
1479
+ * case, and it has a varchar(500) waiting for it. Refusing it would be a
1480
+ * regression dressed up as a fix.
1481
+ */
1482
+ it("accepts a four-hundred character v3 key, which is an ordinary passphrase and fits its column", () => {
1483
+ const value: string = "k".repeat(400);
1484
+
1485
+ for (const [key] of KEY_CREDENTIAL_FIELDS) {
1486
+ expect(
1487
+ SnmpScanConfigUtil.getValidationError([configWithField(key, value)]),
1488
+ ).toBeNull();
1489
+ }
1490
+ });
1491
+
1492
+ it("accepts both v3 keys at exactly the LongText bound", () => {
1493
+ const value: string = "k".repeat(MAX_SNMP_CONFIG_KEY_LENGTH);
1494
+
1495
+ expect(value).toHaveLength(500);
1496
+
1497
+ for (const [key] of KEY_CREDENTIAL_FIELDS) {
1498
+ expect(
1499
+ SnmpScanConfigUtil.getValidationError([configWithField(key, value)]),
1500
+ ).toBeNull();
1501
+ }
1502
+ });
1503
+
1504
+ it("refuses both v3 keys one character over the LongText bound, naming the card and quoting both lengths", () => {
1505
+ const value: string = "k".repeat(MAX_SNMP_CONFIG_KEY_LENGTH + 1);
1506
+
1507
+ expect(value).toHaveLength(501);
1508
+
1509
+ for (const [key, label] of KEY_CREDENTIAL_FIELDS) {
1510
+ expect(errorForSecondConfig(configWithField(key, value))).toBe(
1511
+ `SNMP config 2: the ${label} cannot be longer than ` +
1512
+ `${MAX_SNMP_CONFIG_KEY_LENGTH} characters. This one is 501.`,
1513
+ );
1514
+ }
1515
+ });
1516
+
1517
+ /*
1518
+ * The two bounds have to stay APART. Collapsing them into one number is
1519
+ * the obvious simplification and it is wrong in both directions: the
1520
+ * ShortText bound applied to a key refuses a legitimate passphrase, and
1521
+ * the LongText bound applied to a community string lets through exactly
1522
+ * the value that raises on the mirrored varchar(100).
1523
+ */
1524
+ it("holds the two bounds apart, so 101 characters is fine in a v3 key and refused in a community string", () => {
1525
+ const value: string = "x".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH + 1);
1526
+
1527
+ expect(
1528
+ SnmpScanConfigUtil.getValidationError([{ snmpV3AuthKey: value }]),
1529
+ ).toBeNull();
1530
+ expect(
1531
+ SnmpScanConfigUtil.getValidationError([{ snmpV3PrivKey: value }]),
1532
+ ).toBeNull();
1533
+ expect(
1534
+ SnmpScanConfigUtil.getValidationError([{ snmpCommunityString: value }]),
1535
+ ).toBe(
1536
+ `SNMP config 1: the community string cannot be longer than ` +
1537
+ `${MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH} characters. This one is 101.`,
1538
+ );
1539
+ });
1540
+
1541
+ /*
1542
+ * Measured after trimming, exactly like the name bound above — and for
1543
+ * the same reason: normalizeConfig trims before storing, so what reaches
1544
+ * the mirrored column is the trimmed value, and validating the padded
1545
+ * one would refuse a value that would have fitted.
1546
+ */
1547
+ it("measures every credential field after trimming, exactly as the name bound is measured", () => {
1548
+ for (const [key] of SHORT_TEXT_CREDENTIAL_FIELDS) {
1549
+ const padded: string = ` ${"s".repeat(
1550
+ MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH,
1551
+ )} `;
1552
+
1553
+ expect(padded.length).toBeGreaterThan(
1554
+ MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH,
1555
+ );
1556
+ expect(
1557
+ SnmpScanConfigUtil.getValidationError([configWithField(key, padded)]),
1558
+ ).toBeNull();
1559
+ }
1560
+
1561
+ for (const [key] of KEY_CREDENTIAL_FIELDS) {
1562
+ const padded: string = ` ${"k".repeat(MAX_SNMP_CONFIG_KEY_LENGTH)} `;
1563
+
1564
+ expect(padded.length).toBeGreaterThan(MAX_SNMP_CONFIG_KEY_LENGTH);
1565
+ expect(
1566
+ SnmpScanConfigUtil.getValidationError([configWithField(key, padded)]),
1567
+ ).toBeNull();
1568
+ }
1569
+ });
1570
+
1571
+ // Padding neither rescues an over-long value nor inflates the number quoted back.
1572
+ it("quotes the trimmed length when a padded credential is still too long", () => {
1573
+ const padded: string = ` ${"c".repeat(
1574
+ MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH + 1,
1575
+ )} `;
1576
+
1577
+ expect(padded).toHaveLength(107);
1578
+ expect(errorForSecondConfig({ snmpCommunityString: padded })).toBe(
1579
+ `SNMP config 2: the community string cannot be longer than ` +
1580
+ `${MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH} characters. This one is 101.`,
1581
+ );
1582
+ });
1583
+
1584
+ /*
1585
+ * THE ASYMMETRY THAT WAS THE BUG.
1586
+ *
1587
+ * Only card #1 is mirrored, so only card #1 used to fail — at the
1588
+ * database, in a column's words. The same value anywhere else stored
1589
+ * happily and waited to detonate on a reorder. The rule must therefore be
1590
+ * identical in every position: same wording, same bound, differing only
1591
+ * in the card it names.
1592
+ */
1593
+ it("refuses an over-long community string in a card the mirror never touches, not only in the first card", () => {
1594
+ const communityString: string = "c".repeat(
1595
+ MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH + 1,
1596
+ );
1597
+
1598
+ const inFirstCard: string | null = SnmpScanConfigUtil.getValidationError([
1599
+ { id: "one", snmpCommunityString: communityString },
1600
+ { id: "two" },
1601
+ ]);
1602
+ const inSecondCard: string | null = SnmpScanConfigUtil.getValidationError(
1603
+ [{ id: "one" }, { id: "two", snmpCommunityString: communityString }],
1604
+ );
1605
+
1606
+ expect(inFirstCard).toContain("SNMP config 1:");
1607
+ expect(inSecondCard).toContain("SNMP config 2:");
1608
+ expect(inFirstCard!.replace("SNMP config 1", "SNMP config 2")).toBe(
1609
+ inSecondCard,
1610
+ );
1611
+ });
1612
+
1613
+ /*
1614
+ * The length check runs BEFORE the v3 recognition checks, so an over-long
1615
+ * security level or protocol is reported as too long rather than as
1616
+ * unrecognized. It is both, but only one of those sentences tells the
1617
+ * operator what to do about a value they pasted by accident.
1618
+ */
1619
+ it("reports an over-long v3 protocol as too long rather than as unrecognized", () => {
1620
+ const error: string | null = errorForSecondConfig(
1621
+ validAuthPrivConfig({
1622
+ id: "second",
1623
+ snmpV3AuthProtocol: "a".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH + 1),
1624
+ }),
1625
+ );
1626
+
1627
+ expect(error).toBe(
1628
+ `SNMP config 2: the v3 authentication protocol cannot be longer than ` +
1629
+ `${MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH} characters. This one is 101.`,
1630
+ );
1631
+ expect(error).not.toContain("not a recognized");
1632
+ });
1633
+
1634
+ /*
1635
+ * The whole point, stated as one property: anything getValidationError
1636
+ * lets through must fit the columns getMirroredLegacyColumns writes it
1637
+ * into. This is the assertion that would have caught the original defect
1638
+ * even if every bound above were spelled differently.
1639
+ */
1640
+ it("guarantees that a list which validates mirrors into values that fit their varchar widths", () => {
1641
+ const configs: Array<DiscoveryScanSnmpConfig> = [
1642
+ {
1643
+ id: "i".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH),
1644
+ name: "n".repeat(MAX_SNMP_CONFIG_NAME_LENGTH),
1645
+ snmpCommunityString: "c".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH),
1646
+ snmpV3Username: "u".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH),
1647
+ snmpV3SecurityLevel: "l".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH),
1648
+ snmpV3AuthProtocol: "a".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH),
1649
+ snmpV3PrivProtocol: "p".repeat(MAX_SNMP_CONFIG_SHORT_TEXT_LENGTH),
1650
+ snmpV3AuthKey: "k".repeat(MAX_SNMP_CONFIG_KEY_LENGTH),
1651
+ snmpV3PrivKey: "v".repeat(MAX_SNMP_CONFIG_KEY_LENGTH),
1652
+ },
1653
+ ];
1654
+
1655
+ expect(SnmpScanConfigUtil.getValidationError(configs)).toBeNull();
1656
+
1657
+ const mirrored: Record<string, string | number | null> =
1658
+ SnmpScanConfigUtil.getMirroredLegacyColumns(configs);
1659
+
1660
+ for (const column of MIRRORED_COLUMN_KEYS) {
1661
+ const value: string | number | null | undefined = mirrored[column];
1662
+
1663
+ if (typeof value !== "string") {
1664
+ continue;
1665
+ }
1666
+
1667
+ const columnWidth: number =
1668
+ column === "snmpV3AuthKey" || column === "snmpV3PrivKey"
1669
+ ? ColumnLength.LongText
1670
+ : ColumnLength.ShortText;
1671
+
1672
+ expect(value.length).toBeLessThanOrEqual(columnWidth);
1673
+ }
1674
+ });
1675
+ });
1676
+
1677
+ describe("port", () => {
1678
+ it("accepts a port inside the UDP range, absent, or blank", () => {
1679
+ const ports: Array<unknown> = [
1680
+ MINIMUM_SNMP_PORT,
1681
+ MAXIMUM_SNMP_PORT,
1682
+ 161,
1683
+ 1161,
1684
+ "161",
1685
+ " 161 ",
1686
+ "",
1687
+ " ",
1688
+ null,
1689
+ undefined,
1690
+ ];
1691
+
1692
+ for (const snmpPort of ports) {
1693
+ expect(
1694
+ SnmpScanConfigUtil.getValidationError([{ id: "x", snmpPort }]),
1695
+ ).toBeNull();
1696
+ }
1697
+ });
1698
+
1699
+ it("refuses a port outside the UDP range, naming the position", () => {
1700
+ for (const snmpPort of [0, -1, 65536, 99999, "0", "65536"]) {
1701
+ expect(errorForSecondConfig({ id: "second", snmpPort })).toBe(
1702
+ `SNMP config 2: the SNMP port must be between ${MINIMUM_SNMP_PORT} and ${MAXIMUM_SNMP_PORT}.`,
1703
+ );
1704
+ }
1705
+ });
1706
+
1707
+ /*
1708
+ * Checked against the RAW value, not a parsed one. A Number form field
1709
+ * posts its contents as text, so "161.5" would parseInt to 161, clear
1710
+ * both bounds, and store a port the operator never typed.
1711
+ */
1712
+ it("refuses a port that is not a whole number, including one that would parseInt cleanly", () => {
1713
+ for (const snmpPort of ["161.5", 161.5, "abc", "16a1", Number.NaN]) {
1714
+ expect(errorForSecondConfig({ id: "second", snmpPort })).toBe(
1715
+ "SNMP config 2: the SNMP port must be a whole number.",
1716
+ );
1717
+ }
1718
+ });
1719
+ });
1720
+
1721
+ describe("duplicate ids", () => {
1722
+ /*
1723
+ * Duplicate ids make findById ambiguous, so a host found by the second of
1724
+ * two identically-identified configs could import with the first one's
1725
+ * credentials — the precise bug the id scheme exists to prevent. Only
1726
+ * reachable through an out-of-band write; the form mints one id per card.
1727
+ */
1728
+ it("refuses two configs that share an id, quoting the id", () => {
1729
+ const error: string | null = SnmpScanConfigUtil.getValidationError([
1730
+ { id: "shared-id", name: "Core" },
1731
+ { id: "shared-id", name: "Access" },
1732
+ ]);
1733
+
1734
+ expect(error).toBe(
1735
+ 'Two SNMP configs share the id "shared-id". Each config needs its own.',
1736
+ );
1737
+ });
1738
+
1739
+ // Ids are trimmed before comparison, so " x " and "x" are the same id.
1740
+ it("refuses ids that collide only after trimming", () => {
1741
+ expect(
1742
+ SnmpScanConfigUtil.getValidationError([
1743
+ { id: " shared-id " },
1744
+ { id: "shared-id" },
1745
+ ]),
1746
+ ).toContain('share the id "shared-id"');
1747
+ });
1748
+
1749
+ // A missing id is not a duplicate of another missing id.
1750
+ it("accepts several configs that carry no id at all", () => {
1751
+ expect(
1752
+ SnmpScanConfigUtil.getValidationError([
1753
+ { name: "Core" },
1754
+ { name: "Access" },
1755
+ { name: "Printers" },
1756
+ ]),
1757
+ ).toBeNull();
1758
+ expect(
1759
+ SnmpScanConfigUtil.getValidationError([{ id: "" }, { id: " " }]),
1760
+ ).toBeNull();
1761
+ });
1762
+
1763
+ it("accepts distinct ids", () => {
1764
+ expect(
1765
+ SnmpScanConfigUtil.getValidationError([
1766
+ { id: "a" },
1767
+ { id: "b" },
1768
+ { id: "c" },
1769
+ ]),
1770
+ ).toBeNull();
1771
+ });
1772
+ });
1773
+
1774
+ describe("v1 and v2c", () => {
1775
+ /*
1776
+ * The community string is NOT required, even though the device certainly
1777
+ * needs one, because the probe falls back to "public" — a real and very
1778
+ * common answer for discovery. Requiring it would refuse the single most
1779
+ * useful default.
1780
+ */
1781
+ it("accepts a v1 or v2c config with no community string", () => {
1782
+ for (const snmpVersion of ["V1", "V2c", "1", "2c", undefined]) {
1783
+ expect(
1784
+ SnmpScanConfigUtil.getValidationError([{ id: "x", snmpVersion }]),
1785
+ ).toBeNull();
1786
+ expect(
1787
+ SnmpScanConfigUtil.getValidationError([
1788
+ { id: "x", snmpVersion, snmpCommunityString: "" },
1789
+ ]),
1790
+ ).toBeNull();
1791
+ }
1792
+ });
1793
+
1794
+ /*
1795
+ * The v3 rules apply only to v3 configs. A card left with stale v3 values
1796
+ * from a version the operator switched away from must not block the save.
1797
+ */
1798
+ it("does not apply the v3 rules to a v2c config carrying a stale v3 block", () => {
1799
+ expect(
1800
+ SnmpScanConfigUtil.getValidationError([
1801
+ {
1802
+ id: "x",
1803
+ snmpVersion: "V2c",
1804
+ snmpCommunityString: "public",
1805
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
1806
+ snmpV3Username: "",
1807
+ },
1808
+ ]),
1809
+ ).toBeNull();
1810
+ });
1811
+ });
1812
+
1813
+ describe("v3", () => {
1814
+ it("accepts a fully specified authPriv config", () => {
1815
+ expect(
1816
+ SnmpScanConfigUtil.getValidationError([validAuthPrivConfig()]),
1817
+ ).toBeNull();
1818
+ });
1819
+
1820
+ // The v3 rules fire on either spelling of the version, in any case.
1821
+ it("applies the v3 rules whichever spelling of the version was stored", () => {
1822
+ for (const snmpVersion of ["V3", "v3", "3", " V3 "]) {
1823
+ expect(errorForSecondConfig({ id: "second", snmpVersion })).toContain(
1824
+ "SNMP config 2: SNMP v3 needs a username",
1825
+ );
1826
+ }
1827
+ });
1828
+
1829
+ /*
1830
+ * A v3 session with no security name is rejected by every device, host
1831
+ * after host, and the scan reports zero. Caught here so it is a sentence
1832
+ * on the form instead of a silent zero-result run.
1833
+ */
1834
+ it("refuses a v3 config with no username, naming the position", () => {
1835
+ for (const snmpV3Username of [undefined, null, "", " "]) {
1836
+ expect(
1837
+ errorForSecondConfig({
1838
+ id: "second",
1839
+ snmpVersion: "V3",
1840
+ snmpV3Username,
1841
+ }),
1842
+ ).toBe(
1843
+ "SNMP config 2: SNMP v3 needs a username (the security name configured on the device).",
1844
+ );
1845
+ }
1846
+ });
1847
+
1848
+ /*
1849
+ * An unrecognized security level is the most consequential of the three
1850
+ * to let through: the fallback is not a weaker algorithm, it is no
1851
+ * security at all, and the downgrade itself is invisible.
1852
+ */
1853
+ it("refuses an unrecognized security level and lists the accepted ones", () => {
1854
+ const error: string | null = errorForSecondConfig(
1855
+ validAuthPrivConfig({
1856
+ id: "second",
1857
+ snmpV3SecurityLevel: "auth-priv",
1858
+ }),
1859
+ );
1860
+
1861
+ expect(error).toContain("SNMP config 2");
1862
+ expect(error).toContain(
1863
+ '"auth-priv" is not a recognized SNMP v3 security level',
1864
+ );
1865
+ expect(error).toContain(Object.values(SnmpSecurityLevel).join(", "));
1866
+ });
1867
+
1868
+ it("refuses an unrecognized authentication protocol and lists the accepted ones", () => {
1869
+ const error: string | null = errorForSecondConfig(
1870
+ validAuthPrivConfig({ id: "second", snmpV3AuthProtocol: "SHA3" }),
1871
+ );
1872
+
1873
+ expect(error).toContain("SNMP config 2");
1874
+ expect(error).toContain(
1875
+ '"SHA3" is not a recognized SNMP v3 authentication protocol',
1876
+ );
1877
+ expect(error).toContain(Object.values(SnmpAuthProtocol).join(", "));
1878
+ });
1879
+
1880
+ it("refuses an unrecognized privacy protocol and lists the accepted ones", () => {
1881
+ const error: string | null = errorForSecondConfig(
1882
+ validAuthPrivConfig({ id: "second", snmpV3PrivProtocol: "3DES" }),
1883
+ );
1884
+
1885
+ expect(error).toContain("SNMP config 2");
1886
+ expect(error).toContain(
1887
+ '"3DES" is not a recognized SNMP v3 privacy protocol',
1888
+ );
1889
+ expect(error).toContain(Object.values(SnmpPrivProtocol).join(", "));
1890
+ });
1891
+
1892
+ /*
1893
+ * The spellings the UI labels use, and the casing a hand-written row
1894
+ * produces, are all accepted — the same tolerance the enums' own parsers
1895
+ * have, so the form and the API agree.
1896
+ */
1897
+ it("accepts the alternate spellings the labels and hand-written rows use", () => {
1898
+ expect(
1899
+ SnmpScanConfigUtil.getValidationError([
1900
+ validAuthPrivConfig({
1901
+ snmpV3SecurityLevel: "AuthPriv",
1902
+ snmpV3AuthProtocol: "SHA-256",
1903
+ snmpV3PrivProtocol: "AES-256",
1904
+ }),
1905
+ ]),
1906
+ ).toBeNull();
1907
+ });
1908
+
1909
+ /*
1910
+ * A level that asks for authentication with nothing to authenticate with
1911
+ * does not fail loudly — the session is simply rejected, forever.
1912
+ */
1913
+ it("refuses authNoPriv with no authentication key", () => {
1914
+ const error: string | null = errorForSecondConfig({
1915
+ id: "second",
1916
+ snmpVersion: "V3",
1917
+ snmpV3Username: "monitor",
1918
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthNoPriv,
1919
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA,
1920
+ });
1921
+
1922
+ expect(error).toBe(
1923
+ `SNMP config 2: the "${SnmpSecurityLevel.AuthNoPriv}" security level needs an authentication key.`,
1924
+ );
1925
+ });
1926
+
1927
+ it("refuses authPriv with no authentication key", () => {
1928
+ const error: string | null = errorForSecondConfig(
1929
+ validAuthPrivConfig({ id: "second", snmpV3AuthKey: " " }),
1930
+ );
1931
+
1932
+ expect(error).toBe(
1933
+ `SNMP config 2: the "${SnmpSecurityLevel.AuthPriv}" security level needs an authentication key.`,
1934
+ );
1935
+ });
1936
+
1937
+ it("refuses authPriv with no privacy key", () => {
1938
+ const privKeys: Array<unknown> = [undefined, null, "", " "];
1939
+
1940
+ for (const snmpV3PrivKey of privKeys) {
1941
+ expect(
1942
+ errorForSecondConfig({
1943
+ ...validAuthPrivConfig({ id: "second" }),
1944
+ snmpV3PrivKey,
1945
+ }),
1946
+ ).toBe(
1947
+ `SNMP config 2: the "${SnmpSecurityLevel.AuthPriv}" security level needs a privacy key.`,
1948
+ );
1949
+ }
1950
+ });
1951
+
1952
+ // noAuthNoPriv needs neither key: there is nothing to authenticate with.
1953
+ it("accepts noAuthNoPriv with no keys at all", () => {
1954
+ expect(
1955
+ SnmpScanConfigUtil.getValidationError([
1956
+ {
1957
+ id: "x",
1958
+ snmpVersion: "V3",
1959
+ snmpV3Username: "monitor",
1960
+ snmpV3SecurityLevel: SnmpSecurityLevel.NoAuthNoPriv,
1961
+ },
1962
+ ]),
1963
+ ).toBeNull();
1964
+ });
1965
+
1966
+ /*
1967
+ * An unset security level is "unset", not "unrecognized" — the probe
1968
+ * applies its own default — so a v3 config with only a username saves.
1969
+ */
1970
+ it("accepts a v3 config with a username and no security level", () => {
1971
+ expect(
1972
+ SnmpScanConfigUtil.getValidationError([
1973
+ { id: "x", snmpVersion: "V3", snmpV3Username: "monitor" },
1974
+ ]),
1975
+ ).toBeNull();
1976
+ });
1977
+
1978
+ it("accepts authPriv with both keys supplied", () => {
1979
+ expect(
1980
+ SnmpScanConfigUtil.getValidationError([
1981
+ validAuthPrivConfig({
1982
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
1983
+ snmpV3AuthKey: "auth-key-value",
1984
+ snmpV3PrivKey: "priv-key-value",
1985
+ }),
1986
+ ]),
1987
+ ).toBeNull();
1988
+ });
1989
+ });
1990
+
1991
+ // A realistic mixed subnet: v2c access switches, a v3 core, factory printers.
1992
+ it("accepts the mixed list this feature exists to allow", () => {
1993
+ expect(
1994
+ SnmpScanConfigUtil.getValidationError([
1995
+ {
1996
+ id: "access",
1997
+ name: "Access switches",
1998
+ snmpVersion: "V2c",
1999
+ snmpCommunityString: "access-community",
2000
+ snmpPort: 161,
2001
+ },
2002
+ validAuthPrivConfig({ id: "core", name: "Core" }),
2003
+ {
2004
+ id: "printers",
2005
+ name: "Printers - factory default",
2006
+ snmpVersion: "V1",
2007
+ },
2008
+ ]),
2009
+ ).toBeNull();
2010
+ });
2011
+ });
2012
+
2013
+ describe("SnmpScanConfigUtil.getPortValidationError", () => {
2014
+ it("accepts an absent or blank port, because the column has a default", () => {
2015
+ for (const raw of [undefined, null, "", " "]) {
2016
+ expect(
2017
+ SnmpScanConfigUtil.getPortValidationError(raw, "SNMP config 1"),
2018
+ ).toBeNull();
2019
+ }
2020
+ });
2021
+
2022
+ it("accepts the ends of the UDP range and the SNMP default", () => {
2023
+ for (const raw of [MINIMUM_SNMP_PORT, MAXIMUM_SNMP_PORT, 161, "161"]) {
2024
+ expect(
2025
+ SnmpScanConfigUtil.getPortValidationError(raw, "SNMP config 1"),
2026
+ ).toBeNull();
2027
+ }
2028
+ });
2029
+
2030
+ it("refuses a value that is not a whole number", () => {
2031
+ expect(
2032
+ SnmpScanConfigUtil.getPortValidationError("161.5", "SNMP config 4"),
2033
+ ).toBe("SNMP config 4: the SNMP port must be a whole number.");
2034
+ expect(
2035
+ SnmpScanConfigUtil.getPortValidationError("abc", "SNMP config 4"),
2036
+ ).toBe("SNMP config 4: the SNMP port must be a whole number.");
2037
+ });
2038
+
2039
+ it("refuses a value outside the UDP range", () => {
2040
+ expect(SnmpScanConfigUtil.getPortValidationError(0, "SNMP config 4")).toBe(
2041
+ "SNMP config 4: the SNMP port must be between 1 and 65535.",
2042
+ );
2043
+ expect(
2044
+ SnmpScanConfigUtil.getPortValidationError(65536, "SNMP config 4"),
2045
+ ).toBe("SNMP config 4: the SNMP port must be between 1 and 65535.");
2046
+ });
2047
+
2048
+ // The caller owns the wording of the position, so it is echoed verbatim.
2049
+ it("prefixes the message with whatever position the caller named", () => {
2050
+ expect(
2051
+ SnmpScanConfigUtil.getPortValidationError(0, "The SNMP port"),
2052
+ ).toContain("The SNMP port: ");
2053
+ });
2054
+ });
2055
+
2056
+ describe("SnmpScanConfigUtil.normalizeForStorage", () => {
2057
+ /*
2058
+ * Null for an absent list, which is what the column holds for a scan
2059
+ * configured the old way — so a write that never mentions `snmpConfigs`
2060
+ * leaves the row alone rather than materializing a list nobody asked for.
2061
+ */
2062
+ it("returns null for an empty or non-list value", () => {
2063
+ const values: Array<unknown> = [
2064
+ undefined,
2065
+ null,
2066
+ [],
2067
+ "",
2068
+ "not-a-list",
2069
+ 0,
2070
+ 42,
2071
+ true,
2072
+ { id: "an object is not a list" },
2073
+ ];
2074
+
2075
+ for (const value of values) {
2076
+ expect(SnmpScanConfigUtil.normalizeForStorage(value)).toBeNull();
2077
+ }
2078
+ });
2079
+
2080
+ /*
2081
+ * The index-derived id normalizeConfig falls back to is fine for READING a
2082
+ * row, but storing it would make the id positional: delete the first card
2083
+ * and every id below it now names a different credential set, re-pointing
2084
+ * results the probe has already stamped onto hosts.
2085
+ */
2086
+ it("mints a real unique id for every entry that arrived without one", () => {
2087
+ const configs: Array<DiscoveryScanSnmpConfig> | null =
2088
+ SnmpScanConfigUtil.normalizeForStorage([
2089
+ { name: "Core" },
2090
+ { name: "Access" },
2091
+ { id: " ", name: "Printers" },
2092
+ ]);
2093
+
2094
+ expect(configs).toHaveLength(3);
2095
+
2096
+ const ids: Array<string> = configs!.map(
2097
+ (config: DiscoveryScanSnmpConfig): string => {
2098
+ return config.id as string;
2099
+ },
2100
+ );
2101
+
2102
+ for (const id of ids) {
2103
+ expect(id).toMatch(UUID_PATTERN);
2104
+ // The positional form must never reach storage.
2105
+ expect(id).not.toMatch(POSITIONAL_ID_PATTERN);
2106
+ }
2107
+
2108
+ expect(new Set<string>(ids).size).toBe(3);
2109
+ });
2110
+
2111
+ /*
2112
+ * Two saves of the same list must not produce the same ids — that is what
2113
+ * makes the id an identity rather than a position, and it is why the
2114
+ * mint is a real ObjectID and not a hash of the contents.
2115
+ */
2116
+ it("mints different ids on two separate calls with the same input", () => {
2117
+ const first: Array<DiscoveryScanSnmpConfig> | null =
2118
+ SnmpScanConfigUtil.normalizeForStorage([{ name: "Core" }]);
2119
+ const second: Array<DiscoveryScanSnmpConfig> | null =
2120
+ SnmpScanConfigUtil.normalizeForStorage([{ name: "Core" }]);
2121
+
2122
+ expect(first![0]!.id).not.toBe(second![0]!.id);
2123
+ expect(first![0]!.id).toMatch(UUID_PATTERN);
2124
+ expect(second![0]!.id).toMatch(UUID_PATTERN);
2125
+ });
2126
+
2127
+ /*
2128
+ * An id the form already minted is kept verbatim, because the probe may
2129
+ * already have stamped it onto a discovered host.
2130
+ */
2131
+ it("preserves an id that was supplied, trimming it", () => {
2132
+ const configs: Array<DiscoveryScanSnmpConfig> | null =
2133
+ SnmpScanConfigUtil.normalizeForStorage([
2134
+ { id: "11111111-1111-4111-8111-111111111111", name: "Core" },
2135
+ { id: " keep-me ", name: "Access" },
2136
+ ]);
2137
+
2138
+ expect(configs![0]!.id).toBe("11111111-1111-4111-8111-111111111111");
2139
+ expect(configs![1]!.id).toBe("keep-me");
2140
+ });
2141
+
2142
+ it("normalizes the same way resolve does: trims, drops blanks, fixes the version, drops a bad port", () => {
2143
+ const configs: Array<DiscoveryScanSnmpConfig> | null =
2144
+ SnmpScanConfigUtil.normalizeForStorage([
2145
+ {
2146
+ id: "x",
2147
+ name: " Core switches ",
2148
+ snmpVersion: "3",
2149
+ snmpCommunityString: " ",
2150
+ snmpPort: "65536",
2151
+ snmpV3Username: " monitor ",
2152
+ },
2153
+ ]);
2154
+
2155
+ expect(configs![0]).toEqual({
2156
+ id: "x",
2157
+ name: "Core switches",
2158
+ snmpVersion: "V3",
2159
+ snmpV3Username: "monitor",
2160
+ });
2161
+ });
2162
+
2163
+ it("preserves the operator's order", () => {
2164
+ const configs: Array<DiscoveryScanSnmpConfig> | null =
2165
+ SnmpScanConfigUtil.normalizeForStorage([
2166
+ { id: "c" },
2167
+ { id: "a" },
2168
+ { id: "b" },
2169
+ ]);
2170
+
2171
+ expect(
2172
+ configs!.map((config: DiscoveryScanSnmpConfig): string | undefined => {
2173
+ return config.id;
2174
+ }),
2175
+ ).toEqual(["c", "a", "b"]);
2176
+ });
2177
+
2178
+ /*
2179
+ * Idempotence is what the service's sweep-value comparison rests on: a
2180
+ * no-op Save re-normalizes the stored list and compares it against itself,
2181
+ * and any drift there would retire a running scan for no reason. The first
2182
+ * pass mints the ids; every pass after that must change nothing.
2183
+ */
2184
+ it("is idempotent on its own output once the ids have been minted", () => {
2185
+ const once: Array<DiscoveryScanSnmpConfig> | null =
2186
+ SnmpScanConfigUtil.normalizeForStorage([
2187
+ { name: " Core ", snmpVersion: "v3", snmpV3Username: "monitor" },
2188
+ { id: "supplied", snmpVersion: "1", snmpPort: "161" },
2189
+ ]);
2190
+
2191
+ const twice: Array<DiscoveryScanSnmpConfig> | null =
2192
+ SnmpScanConfigUtil.normalizeForStorage(once);
2193
+
2194
+ expect(twice).toEqual(once);
2195
+ expect(JSON.stringify(twice)).toBe(JSON.stringify(once));
2196
+
2197
+ const thrice: Array<DiscoveryScanSnmpConfig> | null =
2198
+ SnmpScanConfigUtil.normalizeForStorage(twice);
2199
+
2200
+ expect(thrice).toEqual(once);
2201
+ });
2202
+
2203
+ /*
2204
+ * Two structurally-equal lists have to serialize identically, because the
2205
+ * service compares them as JSON to decide whether a save actually changed
2206
+ * the scan's sweep configuration.
2207
+ */
2208
+ it("produces identical JSON for two lists that differ only in whitespace and version spelling", () => {
2209
+ const first: Array<DiscoveryScanSnmpConfig> | null =
2210
+ SnmpScanConfigUtil.normalizeForStorage([
2211
+ { id: "x", name: "Core", snmpVersion: "V3", snmpV3Username: "monitor" },
2212
+ ]);
2213
+ const second: Array<DiscoveryScanSnmpConfig> | null =
2214
+ SnmpScanConfigUtil.normalizeForStorage([
2215
+ {
2216
+ id: " x ",
2217
+ name: " Core ",
2218
+ snmpVersion: "3",
2219
+ snmpV3Username: " monitor ",
2220
+ },
2221
+ ]);
2222
+
2223
+ expect(JSON.stringify(first)).toBe(JSON.stringify(second));
2224
+ });
2225
+
2226
+ /*
2227
+ * Junk entries survive as minimal configs rather than being dropped, which
2228
+ * is safe only because getValidationError refuses them first — the service
2229
+ * validates before it normalizes. Pinned so that ordering stays a
2230
+ * deliberate choice rather than an accident.
2231
+ */
2232
+ it("turns an entry that is not an object into a minimal config, which validation refuses first", () => {
2233
+ expect(SnmpScanConfigUtil.getValidationError([null])).toBe(
2234
+ "SNMP config 1 is not valid.",
2235
+ );
2236
+
2237
+ const configs: Array<DiscoveryScanSnmpConfig> | null =
2238
+ SnmpScanConfigUtil.normalizeForStorage([null]);
2239
+
2240
+ expect(configs).toHaveLength(1);
2241
+ expect(configs![0]!.snmpVersion).toBe("V2c");
2242
+ expect(configs![0]!.id).toMatch(UUID_PATTERN);
2243
+ });
2244
+ });
2245
+
2246
+ describe("SnmpScanConfigUtil.getMirroredLegacyColumns", () => {
2247
+ /*
2248
+ * The mirror is a compatibility guarantee, not redundancy. A probe is
2249
+ * deployed separately from the server and is routinely a version behind; a
2250
+ * probe that has never heard of `snmpConfigs` reads the flattened columns
2251
+ * and nothing else. Without the mirror, saving a multi-config scan would
2252
+ * blank the credentials of every older probe in the fleet.
2253
+ */
2254
+ it("always returns all nine flattened columns", () => {
2255
+ const inputs: Array<Array<DiscoveryScanSnmpConfig>> = [
2256
+ [],
2257
+ [{}],
2258
+ [{ id: "x", snmpVersion: "V3" }],
2259
+ [{ id: "x", snmpCommunityString: "public" }],
2260
+ ];
2261
+
2262
+ for (const configs of inputs) {
2263
+ const mirrored: Record<string, string | number | null> =
2264
+ SnmpScanConfigUtil.getMirroredLegacyColumns(configs);
2265
+
2266
+ expect(Object.keys(mirrored).sort()).toEqual(
2267
+ [...MIRRORED_COLUMN_KEYS].sort(),
2268
+ );
2269
+ expect(Object.keys(mirrored)).toHaveLength(9);
2270
+ }
2271
+ });
2272
+
2273
+ /*
2274
+ * Null for "unset", never an omitted key: these values are also what the
2275
+ * probe-claim endpoint compares its optimistic-concurrency `expectedData`
2276
+ * against, and a key omitted for being empty would read as "unchanged"
2277
+ * instead of "cleared".
2278
+ */
2279
+ it("writes null, not undefined or an absent key, for every unset field", () => {
2280
+ const mirrored: Record<string, string | number | null> =
2281
+ SnmpScanConfigUtil.getMirroredLegacyColumns([{ id: "x" }]);
2282
+
2283
+ expect(mirrored).toEqual({
2284
+ snmpVersion: "V2c",
2285
+ snmpCommunityString: null,
2286
+ snmpPort: null,
2287
+ snmpV3SecurityLevel: null,
2288
+ snmpV3Username: null,
2289
+ snmpV3AuthProtocol: null,
2290
+ snmpV3AuthKey: null,
2291
+ snmpV3PrivProtocol: null,
2292
+ snmpV3PrivKey: null,
2293
+ });
2294
+
2295
+ for (const key of MIRRORED_COLUMN_KEYS) {
2296
+ expect(mirrored[key]).not.toBeUndefined();
2297
+ }
2298
+ });
2299
+
2300
+ it("mirrors every field of a fully populated config", () => {
2301
+ const mirrored: Record<string, string | number | null> =
2302
+ SnmpScanConfigUtil.getMirroredLegacyColumns([
2303
+ {
2304
+ id: "x",
2305
+ name: "Core switches",
2306
+ snmpVersion: "V3",
2307
+ snmpCommunityString: "community-value",
2308
+ snmpPort: 1161,
2309
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
2310
+ snmpV3Username: "monitor",
2311
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA256,
2312
+ snmpV3AuthKey: "auth-key-value",
2313
+ snmpV3PrivProtocol: SnmpPrivProtocol.AES256,
2314
+ snmpV3PrivKey: "priv-key-value",
2315
+ },
2316
+ ]);
2317
+
2318
+ expect(mirrored).toEqual({
2319
+ snmpVersion: "V3",
2320
+ snmpCommunityString: "community-value",
2321
+ snmpPort: 1161,
2322
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
2323
+ snmpV3Username: "monitor",
2324
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA256,
2325
+ snmpV3AuthKey: "auth-key-value",
2326
+ snmpV3PrivProtocol: SnmpPrivProtocol.AES256,
2327
+ snmpV3PrivKey: "priv-key-value",
2328
+ });
2329
+
2330
+ // The operator's label is not a column; it must not leak into the mirror.
2331
+ expect(Object.keys(mirrored)).not.toContain("name");
2332
+ expect(Object.keys(mirrored)).not.toContain("id");
2333
+ });
2334
+
2335
+ /*
2336
+ * The FIRST config specifically, because that is the one an older probe
2337
+ * would have been given under the old single-config UI, and because the
2338
+ * list is ordered by the operator's own preference.
2339
+ */
2340
+ it("takes every value from the first config and ignores the rest", () => {
2341
+ const mirrored: Record<string, string | number | null> =
2342
+ SnmpScanConfigUtil.getMirroredLegacyColumns([
2343
+ {
2344
+ id: "first",
2345
+ snmpVersion: "V2c",
2346
+ snmpCommunityString: "first-community",
2347
+ snmpPort: 161,
2348
+ },
2349
+ {
2350
+ id: "second",
2351
+ snmpVersion: "V3",
2352
+ snmpCommunityString: "second-community",
2353
+ snmpPort: 1161,
2354
+ snmpV3Username: "monitor",
2355
+ snmpV3AuthKey: "second-auth-key",
2356
+ },
2357
+ ]);
2358
+
2359
+ expect(mirrored["snmpVersion"]).toBe("V2c");
2360
+ expect(mirrored["snmpCommunityString"]).toBe("first-community");
2361
+ expect(mirrored["snmpPort"]).toBe(161);
2362
+ // Nothing from the second config reaches the flattened columns.
2363
+ expect(mirrored["snmpV3Username"]).toBeNull();
2364
+ expect(mirrored["snmpV3AuthKey"]).toBeNull();
2365
+ });
2366
+
2367
+ /*
2368
+ * The version column is read by older probes with SnmpVersionUtil.parse,
2369
+ * so it always has to hold a spelling that parses — never a raw "banana"
2370
+ * copied out of the list.
2371
+ */
2372
+ it("always mirrors a stored version spelling, whatever the config held", () => {
2373
+ const cases: Array<[string | undefined, string]> = [
2374
+ ["V1", "V1"],
2375
+ ["1", "V1"],
2376
+ ["v3", "V3"],
2377
+ ["3", "V3"],
2378
+ ["V2c", "V2c"],
2379
+ ["2c", "V2c"],
2380
+ ["banana", "V2c"],
2381
+ ["", "V2c"],
2382
+ [undefined, "V2c"],
2383
+ ];
2384
+
2385
+ for (const [stored, expected] of cases) {
2386
+ expect(
2387
+ SnmpScanConfigUtil.getMirroredLegacyColumns([
2388
+ { id: "x", snmpVersion: stored },
2389
+ ])["snmpVersion"],
2390
+ ).toBe(expected);
2391
+ }
2392
+ });
2393
+
2394
+ /*
2395
+ * An empty list should never reach here (validation refuses one), but the
2396
+ * mirror still has to produce a complete, storable set of columns rather
2397
+ * than throwing inside a write hook.
2398
+ */
2399
+ it("produces a complete set of columns for an empty list rather than throwing", () => {
2400
+ const mirrored: Record<string, string | number | null> =
2401
+ SnmpScanConfigUtil.getMirroredLegacyColumns([]);
2402
+
2403
+ expect(mirrored["snmpVersion"]).toBe("V2c");
2404
+ expect(mirrored["snmpCommunityString"]).toBeNull();
2405
+ expect(mirrored["snmpPort"]).toBeNull();
2406
+ });
2407
+
2408
+ /*
2409
+ * The round trip the service actually performs on every save: resolve the
2410
+ * stored list, mirror its first config onto the flattened columns, and a
2411
+ * probe reading only those columns gets the same credentials the first
2412
+ * config carries.
2413
+ */
2414
+ it("round-trips: mirroring a list then resolving the flattened columns yields the first config's credentials", () => {
2415
+ const configs: Array<DiscoveryScanSnmpConfig> = SnmpScanConfigUtil.resolve(
2416
+ scanWithConfigs([
2417
+ {
2418
+ id: "first",
2419
+ name: "Access switches",
2420
+ snmpVersion: "3",
2421
+ snmpCommunityString: "first-community",
2422
+ snmpPort: 1161,
2423
+ snmpV3Username: "monitor",
2424
+ },
2425
+ { id: "second", snmpCommunityString: "second-community" },
2426
+ ]),
2427
+ );
2428
+
2429
+ const mirrored: Record<string, string | number | null> =
2430
+ SnmpScanConfigUtil.getMirroredLegacyColumns(configs);
2431
+
2432
+ const olderProbeView: Array<DiscoveryScanSnmpConfig> =
2433
+ SnmpScanConfigUtil.resolve(mirrored as unknown as SnmpScanConfigSource);
2434
+
2435
+ expect(olderProbeView).toHaveLength(1);
2436
+ expect(olderProbeView[0]!.id).toBe(LEGACY_SNMP_CONFIG_ID);
2437
+ expect(olderProbeView[0]!.snmpVersion).toBe("V3");
2438
+ expect(olderProbeView[0]!.snmpCommunityString).toBe("first-community");
2439
+ expect(olderProbeView[0]!.snmpPort).toBe(1161);
2440
+ expect(olderProbeView[0]!.snmpV3Username).toBe("monitor");
2441
+ });
2442
+ });