@oneuptime/common 12.0.30 → 12.0.32

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 (645) hide show
  1. package/Models/AnalyticsModels/SecurityEvent.ts +48 -16
  2. package/Models/AnalyticsModels/ThreatIntelIndicator.ts +10 -5
  3. package/Models/DatabaseModels/AlertSeverity.ts +44 -0
  4. package/Models/DatabaseModels/AlertState.ts +0 -2
  5. package/Models/DatabaseModels/DetectionRule.ts +16 -5
  6. package/Models/DatabaseModels/Domain.ts +1 -1
  7. package/Models/DatabaseModels/GoogleSecOpsConnection.ts +14 -5
  8. package/Models/DatabaseModels/Incident.ts +1 -1
  9. package/Models/DatabaseModels/IncidentSeverity.ts +44 -0
  10. package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
  11. package/Models/DatabaseModels/Index.ts +4 -0
  12. package/Models/DatabaseModels/MetricType.ts +0 -2
  13. package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
  14. package/Models/DatabaseModels/MonitorTest.ts +0 -2
  15. package/Models/DatabaseModels/NetworkDevice.ts +234 -5
  16. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +88 -0
  17. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +22 -0
  18. package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +501 -0
  19. package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
  20. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
  21. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
  22. package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
  23. package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
  24. package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
  25. package/Models/DatabaseModels/StatusPageResource.ts +0 -2
  26. package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
  27. package/Models/DatabaseModels/ThreatIntelFeed.ts +16 -5
  28. package/Server/API/GlobalConfigAPI.ts +362 -183
  29. package/Server/API/TelemetryAPI.ts +42 -4
  30. package/Server/EnvironmentConfig.ts +112 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.ts +59 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.ts +25 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  35. package/Server/Middleware/MultipartFormData.ts +118 -32
  36. package/Server/Services/AlertEpisodeLabelRuleEngineService.ts +9 -1
  37. package/Server/Services/AlertEpisodeOnCallRuleEngineService.ts +9 -1
  38. package/Server/Services/AlertEpisodeOwnerRuleEngineService.ts +9 -1
  39. package/Server/Services/AlertEpisodePrivacyRuleEngineService.ts +9 -1
  40. package/Server/Services/AlertGroupingEngineService.ts +9 -1
  41. package/Server/Services/AlertLabelRuleEngineService.ts +9 -1
  42. package/Server/Services/AlertOnCallRuleEngineService.ts +9 -1
  43. package/Server/Services/AlertOwnerRuleEngineService.ts +9 -1
  44. package/Server/Services/AlertPrivacyRuleEngineService.ts +9 -1
  45. package/Server/Services/AlertReminderRuleService.ts +9 -1
  46. package/Server/Services/AutoRemediationRuleEngineService.ts +9 -1
  47. package/Server/Services/CephClusterLabelRuleEngineService.ts +9 -1
  48. package/Server/Services/CephClusterOwnerRuleEngineService.ts +9 -1
  49. package/Server/Services/CloudResourceLabelRuleEngineService.ts +9 -1
  50. package/Server/Services/CloudResourceOwnerRuleEngineService.ts +9 -1
  51. package/Server/Services/DashboardLabelRuleEngineService.ts +9 -1
  52. package/Server/Services/DashboardOwnerRuleEngineService.ts +9 -1
  53. package/Server/Services/DatabaseService.ts +39 -9
  54. package/Server/Services/DockerHostLabelRuleEngineService.ts +9 -1
  55. package/Server/Services/DockerHostOwnerRuleEngineService.ts +9 -1
  56. package/Server/Services/DockerSwarmClusterLabelRuleEngineService.ts +9 -1
  57. package/Server/Services/DockerSwarmClusterOwnerRuleEngineService.ts +9 -1
  58. package/Server/Services/HostLabelRuleEngineService.ts +9 -1
  59. package/Server/Services/HostOwnerRuleEngineService.ts +9 -1
  60. package/Server/Services/IncidentEpisodeLabelRuleEngineService.ts +9 -1
  61. package/Server/Services/IncidentEpisodeOnCallRuleEngineService.ts +9 -1
  62. package/Server/Services/IncidentEpisodeOwnerRuleEngineService.ts +9 -1
  63. package/Server/Services/IncidentEpisodePrivacyRuleEngineService.ts +9 -1
  64. package/Server/Services/IncidentGroupingEngineService.ts +9 -1
  65. package/Server/Services/IncidentLabelRuleEngineService.ts +9 -1
  66. package/Server/Services/IncidentOnCallRuleEngineService.ts +9 -1
  67. package/Server/Services/IncidentOwnerRuleEngineService.ts +9 -1
  68. package/Server/Services/IncidentPrivacyRuleEngineService.ts +9 -1
  69. package/Server/Services/IncidentReminderRuleService.ts +9 -1
  70. package/Server/Services/IncidentSlaRuleService.ts +9 -1
  71. package/Server/Services/IncomingCallPolicyLabelRuleEngineService.ts +9 -1
  72. package/Server/Services/IncomingCallPolicyOwnerRuleEngineService.ts +9 -1
  73. package/Server/Services/Index.ts +4 -0
  74. package/Server/Services/IoTFleetLabelRuleEngineService.ts +9 -1
  75. package/Server/Services/IoTFleetOwnerRuleEngineService.ts +9 -1
  76. package/Server/Services/KubernetesClusterLabelRuleEngineService.ts +9 -1
  77. package/Server/Services/KubernetesClusterOwnerRuleEngineService.ts +9 -1
  78. package/Server/Services/KubernetesResourceService.ts +5 -1
  79. package/Server/Services/MonitorLabelRuleEngineService.ts +9 -1
  80. package/Server/Services/MonitorOwnerRuleEngineService.ts +9 -1
  81. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +35 -0
  82. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +96 -0
  83. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +9 -1
  84. package/Server/Services/NetworkDeviceOidTemplateService.ts +201 -0
  85. package/Server/Services/NetworkDeviceOwnerRuleEngineService.ts +9 -1
  86. package/Server/Services/NetworkDeviceRoleService.ts +90 -0
  87. package/Server/Services/NetworkDeviceService.ts +209 -1
  88. package/Server/Services/OnCallDutyPolicyLabelRuleEngineService.ts +9 -1
  89. package/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.ts +9 -1
  90. package/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.ts +9 -1
  91. package/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.ts +9 -1
  92. package/Server/Services/PodmanHostLabelRuleEngineService.ts +9 -1
  93. package/Server/Services/PodmanHostOwnerRuleEngineService.ts +9 -1
  94. package/Server/Services/ProjectService.ts +85 -0
  95. package/Server/Services/ProxmoxClusterLabelRuleEngineService.ts +9 -1
  96. package/Server/Services/ProxmoxClusterOwnerRuleEngineService.ts +9 -1
  97. package/Server/Services/RumApplicationLabelRuleEngineService.ts +9 -1
  98. package/Server/Services/RumApplicationOwnerRuleEngineService.ts +9 -1
  99. package/Server/Services/RunbookLabelRuleEngineService.ts +9 -1
  100. package/Server/Services/RunbookOwnerRuleEngineService.ts +9 -1
  101. package/Server/Services/RunbookRuleEngineService.ts +9 -1
  102. package/Server/Services/ScheduledMaintenanceLabelRuleEngineService.ts +9 -1
  103. package/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.ts +9 -1
  104. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +9 -1
  105. package/Server/Services/ServerlessFunctionLabelRuleEngineService.ts +9 -1
  106. package/Server/Services/ServerlessFunctionOwnerRuleEngineService.ts +9 -1
  107. package/Server/Services/ServiceLabelRuleEngineService.ts +9 -1
  108. package/Server/Services/ServiceOwnerRuleEngineService.ts +9 -1
  109. package/Server/Services/StatusPageLabelRuleEngineService.ts +9 -1
  110. package/Server/Services/StatusPageMonitorRuleEngineService.ts +7 -0
  111. package/Server/Services/StatusPageOwnerRuleEngineService.ts +9 -1
  112. package/Server/Services/TelemetrySourceMapService.ts +266 -23
  113. package/Server/Services/UserNotificationSettingService.ts +0 -6
  114. package/Server/Services/UserService.ts +27 -0
  115. package/Server/Services/WorkflowLabelRuleEngineService.ts +9 -1
  116. package/Server/Services/WorkflowOwnerRuleEngineService.ts +9 -1
  117. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +186 -0
  118. package/Server/Utils/Monitor/CapturedMetricAttributeUtil.ts +245 -0
  119. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +21 -2
  120. package/Server/Utils/Monitor/MonitorMetricUtil.ts +55 -21
  121. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +32 -0
  122. package/Server/Utils/Monitor/NetworkDeviceMetricUtil.ts +15 -2
  123. package/Server/Utils/Monitor/NetworkDeviceWalkUtil.ts +18 -1
  124. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +10 -0
  125. package/Server/Utils/Monitor/SeriesResourceLabels.ts +34 -0
  126. package/Server/Utils/Rules/RuleEngineRuleRead.ts +48 -0
  127. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +14 -1
  128. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +14 -2
  129. package/Server/Utils/Telemetry/SourceMapResolver.ts +16 -5
  130. package/Server/Utils/Telemetry/StackTraceParser.ts +399 -0
  131. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
  132. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
  133. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
  134. package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
  135. package/Tests/App/Dashboard/EventOverlayHook.test.tsx +125 -0
  136. package/Tests/App/Dashboard/HeaderImageAssets.test.tsx +93 -0
  137. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
  138. package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
  139. package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
  140. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3 -3
  141. package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
  142. package/Tests/App/Dashboard/UserElementEmail.test.tsx +12 -4
  143. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +1 -1
  144. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +14 -0
  145. package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +543 -0
  146. package/Tests/Models/SecurityDomainAccessControl.test.ts +429 -0
  147. package/Tests/Server/API/GlobalConfigLicense.test.ts +657 -0
  148. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +84 -6
  149. package/Tests/Server/API/TelemetryResolveStackTraceAPI.test.ts +805 -0
  150. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +32 -7
  151. package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
  152. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +1144 -0
  153. package/Tests/Server/Middleware/MultipartFormData.test.ts +618 -3
  154. package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
  155. package/Tests/Server/Services/MonitorLabelRuleEngineScale.test.ts +520 -0
  156. package/Tests/Server/Services/NetworkDeviceOidTemplateService.test.ts +332 -0
  157. package/Tests/Server/Services/NetworkDeviceOidTemplateWriteGuards.test.ts +409 -0
  158. package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
  159. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
  160. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
  161. package/Tests/Server/Services/RuleEngineRuleFetchLimit.test.ts +239 -0
  162. package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
  163. package/Tests/Server/Services/TelemetrySourceMapService.test.ts +1210 -2
  164. package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
  165. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
  166. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +182 -0
  167. package/Tests/Server/Types/AnalyticsDatabase/SecurityEventModelPermission.test.ts +255 -0
  168. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +212 -0
  169. package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +413 -0
  170. package/Tests/Server/Utils/BasicCron.test.ts +283 -0
  171. package/Tests/Server/Utils/CodeRepository.test.ts +689 -0
  172. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +495 -0
  173. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +77 -0
  174. package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
  175. package/Tests/Server/Utils/Monitor/CapturedMetricAttributeUtil.test.ts +284 -0
  176. package/Tests/Server/Utils/Monitor/MonitorCustomMetricAttributes.test.ts +503 -0
  177. package/Tests/Server/Utils/Monitor/MonitorTemplatePrototypePollution.test.ts +106 -0
  178. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkLogWritePath.test.ts +33 -4
  179. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +14 -4
  180. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +44 -0
  181. package/Tests/Server/Utils/OpenAPISpec.test.ts +536 -0
  182. package/Tests/Server/Utils/Rules/RuleEngineRuleRead.test.ts +115 -0
  183. package/Tests/Server/Utils/StackTraceParser.test.ts +610 -0
  184. package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
  185. package/Tests/Server/Utils/Telemetry/SourceMapLimits.test.ts +656 -0
  186. package/Tests/Server/Utils/Telemetry/SourceMapResolver.test.ts +100 -0
  187. package/Tests/Types/Monitor/SnmpMonitor/SnmpOidListUtil.test.ts +454 -0
  188. package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
  189. package/Tests/Types/SecurityRolePermissions.test.ts +193 -0
  190. package/Tests/UI/Components/Charts/AnnotationLayout.test.ts +773 -0
  191. package/Tests/UI/Components/Charts/ChartAnnotationRail.test.tsx +789 -0
  192. package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +254 -0
  193. package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
  194. package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
  195. package/Tests/UI/Components/Charts/TimeAnnotation.test.ts +471 -0
  196. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +405 -0
  197. package/Tests/UI/Components/ErrorMessageRefreshControl.test.tsx +83 -0
  198. package/Tests/UI/Components/MarkdownConverters.test.ts +686 -0
  199. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
  200. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
  201. package/Tests/UI/Components/ModelTable/BaseModelTableDeleteAndEmptyStates.test.tsx +341 -0
  202. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
  203. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
  204. package/Tests/UI/Components/OrderedStatesList.test.tsx +5 -5
  205. package/Tests/UI/Components/PendingProjectInvitations.test.tsx +1 -1
  206. package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
  207. package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +195 -0
  208. package/Tests/UI/Components/XAxis.test.ts +270 -5
  209. package/Tests/UI/Components/XAxisDaylightSaving.test.ts +113 -0
  210. package/Tests/UI/Styles/ChartFocusRing.test.ts +153 -0
  211. package/Tests/UI/Styles/ChartFocusRingCoverage.test.tsx +377 -0
  212. package/Tests/UI/Styles/ThemeStylesheet.ts +162 -0
  213. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
  214. package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
  215. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
  216. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
  217. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +507 -0
  218. package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
  219. package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
  220. package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
  221. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +951 -1
  222. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +238 -0
  223. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +1056 -0
  224. package/Tests/Utils/NetworkDiscovery/ReverseDnsNameUtil.test.ts +882 -0
  225. package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
  226. package/Tests/Utils/Slug.test.ts +61 -1
  227. package/Tests/jest.setup.ts +19 -0
  228. package/Types/Date.ts +39 -0
  229. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
  230. package/Types/Monitor/SnmpMonitor/SnmpOidListUtil.ts +333 -0
  231. package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
  232. package/Types/Permission.ts +141 -0
  233. package/Types/Rum/SessionReplay.ts +21 -0
  234. package/Types/Telemetry/SourceMap.ts +8 -0
  235. package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
  236. package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
  237. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
  238. package/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.tsx +176 -0
  239. package/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.ts +745 -0
  240. package/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.tsx +576 -0
  241. package/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.tsx +204 -0
  242. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +554 -545
  243. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +337 -371
  244. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +549 -540
  245. package/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.ts +7 -3
  246. package/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.ts +14 -3
  247. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
  248. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  249. package/UI/Components/Charts/Types/ChartEventKind.ts +13 -0
  250. package/UI/Components/Charts/Types/ReferenceRegionProps.ts +6 -0
  251. package/UI/Components/Charts/Types/TimeReferenceLineProps.ts +20 -3
  252. package/UI/Components/Charts/Utils/TimeAnnotation.ts +69 -35
  253. package/UI/Components/Charts/Utils/XAxis.ts +125 -18
  254. package/UI/Components/EditionLabel/EditionLabel.tsx +280 -26
  255. package/UI/Components/ErrorMessage/ErrorMessage.tsx +21 -10
  256. package/UI/Components/Feed/FeedItem.tsx +4 -5
  257. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -2
  258. package/UI/Components/Header/UserProfile/UserProfile.tsx +1 -2
  259. package/UI/Components/Image/Image.tsx +20 -3
  260. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  261. package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +1 -2
  262. package/UI/Components/ModelTable/BaseModelTable.tsx +200 -39
  263. package/UI/Components/Navbar/NavBar.tsx +62 -13
  264. package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
  265. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
  266. package/UI/Components/SideMenu/SideMenu.tsx +12 -2
  267. package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
  268. package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
  269. package/UI/Components/Table/TableHeader.tsx +64 -48
  270. package/UI/Components/Table/TableRow.tsx +13 -3
  271. package/UI/Styles/Theme.css +38 -0
  272. package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
  273. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
  274. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +271 -0
  275. package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
  276. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
  277. package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
  278. package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
  279. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +6 -0
  280. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +141 -10
  281. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +55 -3
  282. package/Utils/NetworkDiscovery/ReverseDnsNameUtil.ts +151 -0
  283. package/Utils/Rules/RuleEngineLimits.ts +32 -0
  284. package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
  285. package/Utils/Slug.ts +34 -1
  286. package/build/dist/Models/AnalyticsModels/SecurityEvent.js +48 -16
  287. package/build/dist/Models/AnalyticsModels/SecurityEvent.js.map +1 -1
  288. package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js +10 -5
  289. package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js.map +1 -1
  290. package/build/dist/Models/DatabaseModels/AlertSeverity.js +46 -1
  291. package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
  292. package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
  293. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  294. package/build/dist/Models/DatabaseModels/DetectionRule.js +16 -5
  295. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  296. package/build/dist/Models/DatabaseModels/Domain.js +1 -1
  297. package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
  298. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +14 -5
  299. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -1
  300. package/build/dist/Models/DatabaseModels/Incident.js +1 -1
  301. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  302. package/build/dist/Models/DatabaseModels/IncidentSeverity.js +46 -1
  303. package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
  304. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
  305. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  306. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  307. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  308. package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
  309. package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
  310. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
  311. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  312. package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
  313. package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
  314. package/build/dist/Models/DatabaseModels/NetworkDevice.js +239 -6
  315. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  316. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +88 -0
  317. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  318. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  319. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +521 -0
  320. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -0
  321. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
  322. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
  323. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
  324. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  325. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
  326. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  327. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
  328. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  329. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
  330. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
  331. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
  332. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  333. package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
  334. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  335. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
  336. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  337. package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js +16 -5
  338. package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js.map +1 -1
  339. package/build/dist/Server/API/GlobalConfigAPI.js +293 -176
  340. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  341. package/build/dist/Server/API/TelemetryAPI.js +31 -3
  342. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  343. package/build/dist/Server/EnvironmentConfig.js +83 -0
  344. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  345. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.js +26 -0
  346. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.js.map +1 -0
  347. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.js +14 -0
  348. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.js.map +1 -0
  349. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
  350. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
  351. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  352. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  353. package/build/dist/Server/Middleware/MultipartFormData.js +93 -28
  354. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  355. package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js +8 -1
  356. package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js.map +1 -1
  357. package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js +8 -1
  358. package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js.map +1 -1
  359. package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js +8 -1
  360. package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js.map +1 -1
  361. package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js +8 -1
  362. package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js.map +1 -1
  363. package/build/dist/Server/Services/AlertGroupingEngineService.js +8 -1
  364. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
  365. package/build/dist/Server/Services/AlertLabelRuleEngineService.js +8 -1
  366. package/build/dist/Server/Services/AlertLabelRuleEngineService.js.map +1 -1
  367. package/build/dist/Server/Services/AlertOnCallRuleEngineService.js +8 -1
  368. package/build/dist/Server/Services/AlertOnCallRuleEngineService.js.map +1 -1
  369. package/build/dist/Server/Services/AlertOwnerRuleEngineService.js +8 -1
  370. package/build/dist/Server/Services/AlertOwnerRuleEngineService.js.map +1 -1
  371. package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js +8 -1
  372. package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js.map +1 -1
  373. package/build/dist/Server/Services/AlertReminderRuleService.js +8 -1
  374. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  375. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +8 -1
  376. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  377. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js +8 -1
  378. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js.map +1 -1
  379. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js +8 -1
  380. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js.map +1 -1
  381. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js +8 -1
  382. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js.map +1 -1
  383. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js +8 -1
  384. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js.map +1 -1
  385. package/build/dist/Server/Services/DashboardLabelRuleEngineService.js +8 -1
  386. package/build/dist/Server/Services/DashboardLabelRuleEngineService.js.map +1 -1
  387. package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js +8 -1
  388. package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js.map +1 -1
  389. package/build/dist/Server/Services/DatabaseService.js +31 -5
  390. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  391. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js +8 -1
  392. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js.map +1 -1
  393. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js +8 -1
  394. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js.map +1 -1
  395. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js +8 -1
  396. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js.map +1 -1
  397. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js +8 -1
  398. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js.map +1 -1
  399. package/build/dist/Server/Services/HostLabelRuleEngineService.js +8 -1
  400. package/build/dist/Server/Services/HostLabelRuleEngineService.js.map +1 -1
  401. package/build/dist/Server/Services/HostOwnerRuleEngineService.js +8 -1
  402. package/build/dist/Server/Services/HostOwnerRuleEngineService.js.map +1 -1
  403. package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js +8 -1
  404. package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js.map +1 -1
  405. package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js +8 -1
  406. package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js.map +1 -1
  407. package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js +8 -1
  408. package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js.map +1 -1
  409. package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js +8 -1
  410. package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js.map +1 -1
  411. package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -1
  412. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  413. package/build/dist/Server/Services/IncidentLabelRuleEngineService.js +8 -1
  414. package/build/dist/Server/Services/IncidentLabelRuleEngineService.js.map +1 -1
  415. package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js +8 -1
  416. package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js.map +1 -1
  417. package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js +8 -1
  418. package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js.map +1 -1
  419. package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js +8 -1
  420. package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js.map +1 -1
  421. package/build/dist/Server/Services/IncidentReminderRuleService.js +8 -1
  422. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  423. package/build/dist/Server/Services/IncidentSlaRuleService.js +8 -1
  424. package/build/dist/Server/Services/IncidentSlaRuleService.js.map +1 -1
  425. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js +8 -1
  426. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js.map +1 -1
  427. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js +8 -1
  428. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js.map +1 -1
  429. package/build/dist/Server/Services/Index.js +4 -0
  430. package/build/dist/Server/Services/Index.js.map +1 -1
  431. package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js +8 -1
  432. package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js.map +1 -1
  433. package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js +8 -1
  434. package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js.map +1 -1
  435. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js +8 -1
  436. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js.map +1 -1
  437. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js +8 -1
  438. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js.map +1 -1
  439. package/build/dist/Server/Services/KubernetesResourceService.js +5 -1
  440. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  441. package/build/dist/Server/Services/MonitorLabelRuleEngineService.js +8 -1
  442. package/build/dist/Server/Services/MonitorLabelRuleEngineService.js.map +1 -1
  443. package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js +8 -1
  444. package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js.map +1 -1
  445. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +30 -21
  446. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  447. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +64 -0
  448. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
  449. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +8 -1
  450. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  451. package/build/dist/Server/Services/NetworkDeviceOidTemplateService.js +180 -0
  452. package/build/dist/Server/Services/NetworkDeviceOidTemplateService.js.map +1 -0
  453. package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js +8 -1
  454. package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js.map +1 -1
  455. package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
  456. package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
  457. package/build/dist/Server/Services/NetworkDeviceService.js +154 -1
  458. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  459. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js +8 -1
  460. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js.map +1 -1
  461. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js +8 -1
  462. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js.map +1 -1
  463. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js +8 -1
  464. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js.map +1 -1
  465. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js +8 -1
  466. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js.map +1 -1
  467. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js +8 -1
  468. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js.map +1 -1
  469. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js +8 -1
  470. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js.map +1 -1
  471. package/build/dist/Server/Services/ProjectService.js +65 -0
  472. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  473. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js +8 -1
  474. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js.map +1 -1
  475. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js +8 -1
  476. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js.map +1 -1
  477. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js +8 -1
  478. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js.map +1 -1
  479. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js +8 -1
  480. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js.map +1 -1
  481. package/build/dist/Server/Services/RunbookLabelRuleEngineService.js +8 -1
  482. package/build/dist/Server/Services/RunbookLabelRuleEngineService.js.map +1 -1
  483. package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js +8 -1
  484. package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js.map +1 -1
  485. package/build/dist/Server/Services/RunbookRuleEngineService.js +8 -1
  486. package/build/dist/Server/Services/RunbookRuleEngineService.js.map +1 -1
  487. package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js +8 -1
  488. package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js.map +1 -1
  489. package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js +8 -1
  490. package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js.map +1 -1
  491. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +8 -1
  492. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  493. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js +8 -1
  494. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js.map +1 -1
  495. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js +8 -1
  496. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js.map +1 -1
  497. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js +8 -1
  498. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js.map +1 -1
  499. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js +8 -1
  500. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js.map +1 -1
  501. package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js +8 -1
  502. package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js.map +1 -1
  503. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +6 -0
  504. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -1
  505. package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js +8 -1
  506. package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js.map +1 -1
  507. package/build/dist/Server/Services/TelemetrySourceMapService.js +213 -19
  508. package/build/dist/Server/Services/TelemetrySourceMapService.js.map +1 -1
  509. package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
  510. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  511. package/build/dist/Server/Services/UserService.js +25 -0
  512. package/build/dist/Server/Services/UserService.js.map +1 -1
  513. package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js +8 -1
  514. package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js.map +1 -1
  515. package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js +8 -1
  516. package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js.map +1 -1
  517. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +145 -0
  518. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +1 -0
  519. package/build/dist/Server/Utils/Monitor/CapturedMetricAttributeUtil.js +201 -0
  520. package/build/dist/Server/Utils/Monitor/CapturedMetricAttributeUtil.js.map +1 -0
  521. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +19 -2
  522. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  523. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +42 -23
  524. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  525. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +29 -0
  526. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  527. package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js +11 -2
  528. package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js.map +1 -1
  529. package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js +16 -1
  530. package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js.map +1 -1
  531. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +10 -0
  532. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  533. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +33 -0
  534. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  535. package/build/dist/Server/Utils/Rules/RuleEngineRuleRead.js +37 -0
  536. package/build/dist/Server/Utils/Rules/RuleEngineRuleRead.js.map +1 -0
  537. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +12 -1
  538. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -1
  539. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +14 -2
  540. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  541. package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js +15 -5
  542. package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js.map +1 -1
  543. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +307 -0
  544. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -1
  545. package/build/dist/Types/Date.js +27 -0
  546. package/build/dist/Types/Date.js.map +1 -1
  547. package/build/dist/Types/Monitor/SnmpMonitor/SnmpOidListUtil.js +253 -0
  548. package/build/dist/Types/Monitor/SnmpMonitor/SnmpOidListUtil.js.map +1 -0
  549. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
  550. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
  551. package/build/dist/Types/Permission.js +126 -0
  552. package/build/dist/Types/Permission.js.map +1 -1
  553. package/build/dist/Types/Rum/SessionReplay.js +19 -0
  554. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  555. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  556. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  557. package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
  558. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  559. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
  560. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  561. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.js +83 -0
  562. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.js.map +1 -0
  563. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.js +531 -0
  564. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.js.map +1 -0
  565. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.js +264 -0
  566. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.js.map +1 -0
  567. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.js +122 -0
  568. package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.js.map +1 -0
  569. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +262 -234
  570. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  571. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +146 -153
  572. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  573. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +261 -233
  574. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  575. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
  576. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
  577. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  578. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  579. package/build/dist/UI/Components/Charts/Types/ChartEventKind.js +14 -0
  580. package/build/dist/UI/Components/Charts/Types/ChartEventKind.js.map +1 -0
  581. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +57 -28
  582. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
  583. package/build/dist/UI/Components/Charts/Utils/XAxis.js +103 -17
  584. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  585. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +198 -23
  586. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  587. package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js +15 -5
  588. package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js.map +1 -1
  589. package/build/dist/UI/Components/Feed/FeedItem.js +4 -5
  590. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  591. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -2
  592. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  593. package/build/dist/UI/Components/Header/UserProfile/UserProfile.js.map +1 -1
  594. package/build/dist/UI/Components/Image/Image.js +6 -1
  595. package/build/dist/UI/Components/Image/Image.js.map +1 -1
  596. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  597. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  598. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +1 -2
  599. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
  600. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +143 -36
  601. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  602. package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
  603. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  604. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  605. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  606. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
  607. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  608. package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
  609. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  610. package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
  611. package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
  612. package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
  613. package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
  614. package/build/dist/UI/Components/Table/TableHeader.js +20 -22
  615. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  616. package/build/dist/UI/Components/Table/TableRow.js +13 -4
  617. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  618. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
  619. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
  620. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
  621. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
  622. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +153 -0
  623. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +1 -0
  624. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
  625. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
  626. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
  627. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  628. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
  629. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  630. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
  631. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
  632. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -1
  633. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +127 -7
  634. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -1
  635. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +50 -3
  636. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  637. package/build/dist/Utils/NetworkDiscovery/ReverseDnsNameUtil.js +132 -0
  638. package/build/dist/Utils/NetworkDiscovery/ReverseDnsNameUtil.js.map +1 -0
  639. package/build/dist/Utils/Rules/RuleEngineLimits.js +31 -0
  640. package/build/dist/Utils/Rules/RuleEngineLimits.js.map +1 -0
  641. package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
  642. package/build/dist/Utils/Slug.js +31 -1
  643. package/build/dist/Utils/Slug.js.map +1 -1
  644. package/jest.config.json +1 -0
  645. package/package.json +2 -2
@@ -0,0 +1,1144 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ jest,
8
+ } from "@jest/globals";
9
+
10
+ /*
11
+ * The attempt limiter guarding the anonymous credential routes -- POST /login,
12
+ * the three second-step routes, and POST /verify-backup-code.
13
+ *
14
+ * What is worth asserting here is not "does it count". It is the handful of
15
+ * properties that decide whether the limit bounds anything at all:
16
+ *
17
+ * - the client address comes from the hop OUR proxy wrote, not the one the
18
+ * caller can forge, or a header buys a fresh bucket every request;
19
+ * - the account key is read out of BOTH shapes `email` legitimately arrives
20
+ * in, because the shipped clients disagree, and reading only one of them
21
+ * would drop every dashboard login into the shared "none" bucket;
22
+ * - rotating the email address still runs into the per-address ceiling;
23
+ * - the window does not slide forward under sustained load, which would make
24
+ * a tripped limit a permanent lockout;
25
+ * - the buckets are separate, so failing the password step cannot spend the
26
+ * recovery step's budget;
27
+ * - a missing counter fails CLOSED, unlike the verification-code limiter.
28
+ *
29
+ * Each has its own describe block below.
30
+ */
31
+
32
+ jest.mock("../../../Server/Infrastructure/Redis", () => {
33
+ return {
34
+ __esModule: true,
35
+ default: {
36
+ getClient: jest.fn(),
37
+ isConnected: jest.fn(),
38
+ },
39
+ };
40
+ });
41
+
42
+ jest.mock("../../../Server/Utils/Logger", () => {
43
+ return {
44
+ __esModule: true,
45
+ default: {
46
+ debug: jest.fn(),
47
+ info: jest.fn(),
48
+ warn: jest.fn(),
49
+ error: jest.fn(),
50
+ },
51
+ getLogAttributesFromRequest: jest.fn().mockReturnValue({}),
52
+ };
53
+ });
54
+
55
+ jest.mock("../../../Server/Utils/Response", () => {
56
+ return {
57
+ __esModule: true,
58
+ default: {
59
+ sendErrorResponse: jest.fn(),
60
+ },
61
+ };
62
+ });
63
+
64
+ import Redis from "../../../Server/Infrastructure/Redis";
65
+ import Response from "../../../Server/Utils/Response";
66
+ import logger from "../../../Server/Utils/Logger";
67
+ import IdentityRateLimit, {
68
+ IdentityRateLimitBucket,
69
+ IdentityRateLimitDecision,
70
+ IdentityRateLimitOutcome,
71
+ IdentityRateLimitScope,
72
+ } from "../../../Server/Middleware/IdentityRateLimit";
73
+ import Exception from "../../../Types/Exception/Exception";
74
+ import ExceptionCode from "../../../Types/Exception/ExceptionCode";
75
+ import {
76
+ ExpressRequest,
77
+ ExpressResponse,
78
+ NextFunction,
79
+ } from "../../../Server/Utils/Express";
80
+
81
+ type MockedFn = ReturnType<typeof jest.fn>;
82
+
83
+ const getClientMock: MockedFn = Redis.getClient as unknown as MockedFn;
84
+ const isConnectedMock: MockedFn = Redis.isConnected as unknown as MockedFn;
85
+ const sendErrorResponseMock: MockedFn =
86
+ Response.sendErrorResponse as unknown as MockedFn;
87
+ const loggerWarnMock: MockedFn = logger.warn as unknown as MockedFn;
88
+ const loggerErrorMock: MockedFn = logger.error as unknown as MockedFn;
89
+
90
+ /*
91
+ * The shipped defaults. Asserted directly in one test below so that a change
92
+ * to any of them has to be made deliberately rather than drifting.
93
+ */
94
+ const WINDOW_SECONDS: number = 15 * 60;
95
+ const PER_ACCOUNT_LIMIT: number = 10;
96
+ const PER_IP_LIMIT: number = 150;
97
+
98
+ /* Mirrors the limiter's own TTL_MULTIPLIER. */
99
+ const TTL_MULTIPLIER: number = 2;
100
+
101
+ /*
102
+ * A fake that behaves like a Redis counter store rather than a bag of
103
+ * assertions: INCR really increments and EXPIRE really records a TTL, so the
104
+ * window and reset tests exercise the limiter's arithmetic instead of
105
+ * restating it.
106
+ */
107
+ interface RecordedExpire {
108
+ key: string;
109
+ ttlSeconds: number;
110
+ }
111
+
112
+ class FakeRedisClient {
113
+ public counters: Map<string, number> = new Map();
114
+ public expires: Array<RecordedExpire> = [];
115
+
116
+ /* Set to make the next exec() reject, for the error-path tests. */
117
+ public failNextExec: Error | null = null;
118
+
119
+ /* Set to make the next exec() resolve to something malformed. */
120
+ public malformedExecResult: unknown | undefined = undefined;
121
+
122
+ public incrCallCount: number = 0;
123
+ public pipelineCallCount: number = 0;
124
+
125
+ public pipeline(): FakePipeline {
126
+ this.pipelineCallCount++;
127
+ return new FakePipeline(this);
128
+ }
129
+
130
+ public reset(): void {
131
+ this.counters.clear();
132
+ this.expires = [];
133
+ this.failNextExec = null;
134
+ this.malformedExecResult = undefined;
135
+ this.incrCallCount = 0;
136
+ this.pipelineCallCount = 0;
137
+ }
138
+
139
+ public expiresForKey(key: string): Array<RecordedExpire> {
140
+ return this.expires.filter((recorded: RecordedExpire) => {
141
+ return recorded.key === key;
142
+ });
143
+ }
144
+
145
+ public keysMatching(fragment: string): Array<string> {
146
+ return Array.from(this.counters.keys()).filter((key: string) => {
147
+ return key.includes(fragment);
148
+ });
149
+ }
150
+ }
151
+
152
+ type QueuedCommand = () => [Error | null, unknown];
153
+
154
+ class FakePipeline {
155
+ private commands: Array<QueuedCommand> = [];
156
+
157
+ public constructor(private client: FakeRedisClient) {}
158
+
159
+ public incr(key: string): FakePipeline {
160
+ this.commands.push((): [Error | null, unknown] => {
161
+ this.client.incrCallCount++;
162
+ const next: number = (this.client.counters.get(key) || 0) + 1;
163
+ this.client.counters.set(key, next);
164
+ return [null, next];
165
+ });
166
+
167
+ return this;
168
+ }
169
+
170
+ public expire(key: string, ttlSeconds: number): FakePipeline {
171
+ this.commands.push((): [Error | null, unknown] => {
172
+ this.client.expires.push({ key, ttlSeconds });
173
+ return [null, 1];
174
+ });
175
+
176
+ return this;
177
+ }
178
+
179
+ public async exec(): Promise<unknown> {
180
+ if (this.client.failNextExec) {
181
+ const error: Error = this.client.failNextExec;
182
+ this.client.failNextExec = null;
183
+ throw error;
184
+ }
185
+
186
+ if (typeof this.client.malformedExecResult !== "undefined") {
187
+ const result: unknown = this.client.malformedExecResult;
188
+ this.client.malformedExecResult = undefined;
189
+ return result;
190
+ }
191
+
192
+ return this.commands.map((command: QueuedCommand) => {
193
+ return command();
194
+ });
195
+ }
196
+ }
197
+
198
+ interface BuildRequestOverrides {
199
+ body?: unknown;
200
+ headers?: Record<string, string | Array<string>>;
201
+ socketAddress?: string | undefined;
202
+ ip?: string | undefined;
203
+ }
204
+
205
+ const buildRequest: (overrides?: BuildRequestOverrides) => ExpressRequest = (
206
+ overrides: BuildRequestOverrides = {},
207
+ ) => {
208
+ return {
209
+ body: overrides.body,
210
+ headers: overrides.headers || {},
211
+ socket: { remoteAddress: overrides.socketAddress },
212
+ ip: overrides.ip,
213
+ } as unknown as ExpressRequest;
214
+ };
215
+
216
+ interface BuiltResponse {
217
+ response: ExpressResponse;
218
+ headers: Record<string, string>;
219
+ }
220
+
221
+ const buildResponse: () => BuiltResponse = () => {
222
+ const headers: Record<string, string> = {};
223
+
224
+ const response: ExpressResponse = {
225
+ setHeader: (name: string, value: string): void => {
226
+ headers[name] = value;
227
+ },
228
+ } as unknown as ExpressResponse;
229
+
230
+ return { response, headers };
231
+ };
232
+
233
+ describe("IdentityRateLimit", () => {
234
+ let client: FakeRedisClient;
235
+ let nowSpy: ReturnType<typeof jest.spyOn>;
236
+ let currentTime: number;
237
+
238
+ beforeEach(() => {
239
+ jest.clearAllMocks();
240
+
241
+ client = new FakeRedisClient();
242
+ getClientMock.mockReturnValue(client);
243
+ isConnectedMock.mockReturnValue(true);
244
+
245
+ /*
246
+ * Pinned to the start of a window so counters and Retry-After maths are
247
+ * deterministic. Tests that care about window boundaries move it.
248
+ */
249
+ currentTime = 1_700_000_000_000;
250
+ currentTime = currentTime - (currentTime % (WINDOW_SECONDS * 1000));
251
+ nowSpy = jest.spyOn(Date, "now").mockImplementation(() => {
252
+ return currentTime;
253
+ });
254
+
255
+ /*
256
+ * The unavailable-log throttle is static state that outlives a test, so
257
+ * every test starts from a clock far enough past the last log for the
258
+ * next one to be allowed.
259
+ */
260
+ currentTime = currentTime + 0;
261
+ });
262
+
263
+ afterEach(() => {
264
+ nowSpy.mockRestore();
265
+ });
266
+
267
+ const consume: (data: {
268
+ accountKey?: string;
269
+ clientIp?: string;
270
+ bucket?: IdentityRateLimitBucket;
271
+ }) => Promise<IdentityRateLimitDecision> = (data: {
272
+ accountKey?: string;
273
+ clientIp?: string;
274
+ bucket?: IdentityRateLimitBucket;
275
+ }) => {
276
+ return IdentityRateLimit.consume({
277
+ accountKey: data.accountKey || "user@example.com",
278
+ clientIp: data.clientIp || "203.0.113.7",
279
+ bucket: data.bucket || IdentityRateLimitBucket.Login,
280
+ });
281
+ };
282
+
283
+ describe("resolveClientIp", () => {
284
+ /*
285
+ * The security-critical one. Nginx uses $proxy_add_x_forwarded_for, which
286
+ * APPENDS the peer address -- so a caller who sends their own
287
+ * X-Forwarded-For controls the LEFT of the list. Keying on the leftmost
288
+ * entry means a header per request is a bucket per request, which is no
289
+ * limit at all.
290
+ */
291
+ it("reads the address our proxy appended, not the one the caller sent", () => {
292
+ const request: ExpressRequest = buildRequest({
293
+ headers: { "x-forwarded-for": "9.9.9.9, 203.0.113.7" },
294
+ });
295
+
296
+ expect(IdentityRateLimit.resolveClientIp(request)).toBe("203.0.113.7");
297
+ });
298
+
299
+ it("gives a caller rotating the forged left-hand entry one single bucket", () => {
300
+ const first: string = IdentityRateLimit.resolveClientIp(
301
+ buildRequest({
302
+ headers: { "x-forwarded-for": "1.1.1.1, 203.0.113.7" },
303
+ }),
304
+ );
305
+ const second: string = IdentityRateLimit.resolveClientIp(
306
+ buildRequest({
307
+ headers: { "x-forwarded-for": "2.2.2.2, 203.0.113.7" },
308
+ }),
309
+ );
310
+
311
+ expect(first).toBe(second);
312
+ });
313
+
314
+ it("uses the sole entry when the header carries exactly one hop", () => {
315
+ expect(
316
+ IdentityRateLimit.resolveClientIp(
317
+ buildRequest({ headers: { "x-forwarded-for": "203.0.113.7" } }),
318
+ ),
319
+ ).toBe("203.0.113.7");
320
+ });
321
+
322
+ it("joins a repeated header in wire order and still reads the right end", () => {
323
+ expect(
324
+ IdentityRateLimit.resolveClientIp(
325
+ buildRequest({
326
+ headers: { "x-forwarded-for": ["9.9.9.9", "203.0.113.7"] },
327
+ }),
328
+ ),
329
+ ).toBe("203.0.113.7");
330
+ });
331
+
332
+ it("falls back to the transport peer when the header is absent", () => {
333
+ expect(
334
+ IdentityRateLimit.resolveClientIp(
335
+ buildRequest({ socketAddress: "198.51.100.4" }),
336
+ ),
337
+ ).toBe("198.51.100.4");
338
+ });
339
+
340
+ /*
341
+ * Node reports the IPv4-mapped form on a dual-stack listener. Left as-is
342
+ * one caller would land in two buckets depending on which shape their
343
+ * address arrived in.
344
+ */
345
+ it("normalizes an IPv4-mapped IPv6 peer to the bare IPv4 literal", () => {
346
+ expect(
347
+ IdentityRateLimit.resolveClientIp(
348
+ buildRequest({ socketAddress: "::ffff:198.51.100.4" }),
349
+ ),
350
+ ).toBe("198.51.100.4");
351
+ });
352
+
353
+ it("shares one bucket across everything with no resolvable address", () => {
354
+ expect(IdentityRateLimit.resolveClientIp(buildRequest({}))).toBe(
355
+ "unknown",
356
+ );
357
+ });
358
+
359
+ it("refuses to trust an unparseable value in the trusted position", () => {
360
+ expect(
361
+ IdentityRateLimit.resolveClientIp(
362
+ buildRequest({ headers: { "x-forwarded-for": "1.2.3.4, garbage" } }),
363
+ ),
364
+ ).toBe("unknown");
365
+ });
366
+
367
+ it("keeps a hostile header out of the Redis key charset", () => {
368
+ const resolved: string = IdentityRateLimit.resolveClientIp(
369
+ buildRequest({ socketAddress: "1.2.3.4" }),
370
+ );
371
+
372
+ expect(resolved).toMatch(/^[a-zA-Z0-9._:%\-[\]@]*$/);
373
+ });
374
+ });
375
+
376
+ describe("resolveAccountKey", () => {
377
+ /*
378
+ * `email` legitimately arrives in two shapes and the shipped clients
379
+ * disagree: the dashboard's /login sends the serialized Email envelope,
380
+ * its /verify-totp-auth sends a bare string. Both must key the same, or
381
+ * the per-account budget can be taken twice by alternating them.
382
+ */
383
+ it("reads the bare string shape the second-step routes send", () => {
384
+ expect(
385
+ IdentityRateLimit.resolveAccountKey(
386
+ buildRequest({ body: { data: { email: "user@example.com" } } }),
387
+ ),
388
+ ).toBe("user@example.com");
389
+ });
390
+
391
+ it("reads the serialized Email envelope that /login sends", () => {
392
+ expect(
393
+ IdentityRateLimit.resolveAccountKey(
394
+ buildRequest({
395
+ body: {
396
+ data: { email: { _type: "Email", value: "user@example.com" } },
397
+ },
398
+ }),
399
+ ),
400
+ ).toBe("user@example.com");
401
+ });
402
+
403
+ it("keys both shapes identically, so alternating them buys nothing", () => {
404
+ const fromString: string = IdentityRateLimit.resolveAccountKey(
405
+ buildRequest({ body: { data: { email: "user@example.com" } } }),
406
+ );
407
+ const fromEnvelope: string = IdentityRateLimit.resolveAccountKey(
408
+ buildRequest({
409
+ body: {
410
+ data: { email: { _type: "Email", value: "user@example.com" } },
411
+ },
412
+ }),
413
+ );
414
+
415
+ expect(fromString).toBe(fromEnvelope);
416
+ });
417
+
418
+ it("unwraps any object carrying a string value, not only _type Email", () => {
419
+ expect(
420
+ IdentityRateLimit.resolveAccountKey(
421
+ buildRequest({
422
+ body: { data: { email: { value: "user@example.com" } } },
423
+ }),
424
+ ),
425
+ ).toBe("user@example.com");
426
+ });
427
+
428
+ it("accepts the flatter top-level shape as well as the nested one", () => {
429
+ expect(
430
+ IdentityRateLimit.resolveAccountKey(
431
+ buildRequest({ body: { email: "user@example.com" } }),
432
+ ),
433
+ ).toBe("user@example.com");
434
+ });
435
+
436
+ it("prefers the nested field when both are present", () => {
437
+ expect(
438
+ IdentityRateLimit.resolveAccountKey(
439
+ buildRequest({
440
+ body: {
441
+ email: "top@example.com",
442
+ data: { email: "nested@example.com" },
443
+ },
444
+ }),
445
+ ),
446
+ ).toBe("nested@example.com");
447
+ });
448
+
449
+ /* Case alone must not buy a fresh allowance. */
450
+ it("lower-cases, so Bob@ and bob@ are one bucket", () => {
451
+ expect(
452
+ IdentityRateLimit.resolveAccountKey(
453
+ buildRequest({ body: { data: { email: "Bob@Example.COM" } } }),
454
+ ),
455
+ ).toBe("bob@example.com");
456
+ });
457
+
458
+ it("trims surrounding whitespace before keying", () => {
459
+ expect(
460
+ IdentityRateLimit.resolveAccountKey(
461
+ buildRequest({ body: { data: { email: " user@example.com " } } }),
462
+ ),
463
+ ).toBe("user@example.com");
464
+ });
465
+
466
+ it("buckets a missing body rather than throwing", () => {
467
+ expect(IdentityRateLimit.resolveAccountKey(buildRequest({}))).toBe(
468
+ "none",
469
+ );
470
+ });
471
+
472
+ it("buckets a non-string email rather than throwing", () => {
473
+ expect(
474
+ IdentityRateLimit.resolveAccountKey(
475
+ buildRequest({ body: { data: { email: 12345 } } }),
476
+ ),
477
+ ).toBe("none");
478
+ });
479
+
480
+ it("buckets an array email rather than indexing it", () => {
481
+ expect(
482
+ IdentityRateLimit.resolveAccountKey(
483
+ buildRequest({ body: { data: { email: ["a@example.com"] } } }),
484
+ ),
485
+ ).toBe("none");
486
+ });
487
+
488
+ it("buckets a whitespace-only email", () => {
489
+ expect(
490
+ IdentityRateLimit.resolveAccountKey(
491
+ buildRequest({ body: { data: { email: " " } } }),
492
+ ),
493
+ ).toBe("none");
494
+ });
495
+
496
+ it("buckets a null data envelope rather than throwing", () => {
497
+ expect(
498
+ IdentityRateLimit.resolveAccountKey(
499
+ buildRequest({ body: { data: null } }),
500
+ ),
501
+ ).toBe("none");
502
+ });
503
+
504
+ /*
505
+ * The key segment is caller-supplied, so it must not be able to carry
506
+ * newlines, spaces or wildcards into a Redis key.
507
+ */
508
+ it("strips anything outside the key charset", () => {
509
+ const key: string = IdentityRateLimit.resolveAccountKey(
510
+ buildRequest({ body: { data: { email: "a b\nc\r*d@example.com" } } }),
511
+ );
512
+
513
+ expect(key).toMatch(/^[a-zA-Z0-9._:%\-[\]@]*$/);
514
+ expect(key).not.toContain("*");
515
+ expect(key).not.toContain("\n");
516
+ });
517
+
518
+ it("bounds the key segment length", () => {
519
+ const key: string = IdentityRateLimit.resolveAccountKey(
520
+ buildRequest({
521
+ body: { data: { email: `${"a".repeat(5000)}@example.com` } },
522
+ }),
523
+ );
524
+
525
+ expect(key.length).toBeLessThanOrEqual(64);
526
+ });
527
+ });
528
+
529
+ describe("getBucketConfig", () => {
530
+ it("ships the documented login budget", () => {
531
+ expect(
532
+ IdentityRateLimit.getBucketConfig(IdentityRateLimitBucket.Login),
533
+ ).toEqual({
534
+ windowSeconds: WINDOW_SECONDS,
535
+ perAccountLimit: PER_ACCOUNT_LIMIT,
536
+ perIpLimit: PER_IP_LIMIT,
537
+ });
538
+ });
539
+
540
+ it("ships the documented two-factor budget", () => {
541
+ expect(
542
+ IdentityRateLimit.getBucketConfig(IdentityRateLimitBucket.TwoFactor),
543
+ ).toEqual({
544
+ windowSeconds: WINDOW_SECONDS,
545
+ perAccountLimit: PER_ACCOUNT_LIMIT,
546
+ perIpLimit: PER_IP_LIMIT,
547
+ });
548
+ });
549
+
550
+ it("ships the documented backup-code budget", () => {
551
+ expect(
552
+ IdentityRateLimit.getBucketConfig(IdentityRateLimitBucket.BackupCode),
553
+ ).toEqual({
554
+ windowSeconds: WINDOW_SECONDS,
555
+ perAccountLimit: PER_ACCOUNT_LIMIT,
556
+ perIpLimit: PER_IP_LIMIT,
557
+ });
558
+ });
559
+
560
+ it("falls back to the login budget for an unknown bucket", () => {
561
+ expect(
562
+ IdentityRateLimit.getBucketConfig(
563
+ "not-a-bucket" as IdentityRateLimitBucket,
564
+ ),
565
+ ).toEqual(
566
+ IdentityRateLimit.getBucketConfig(IdentityRateLimitBucket.Login),
567
+ );
568
+ });
569
+ });
570
+
571
+ describe("the per-account counter", () => {
572
+ it("allows exactly the budget and refuses the one after", async () => {
573
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT; i++) {
574
+ expect((await consume({})).outcome).toBe(
575
+ IdentityRateLimitOutcome.Allowed,
576
+ );
577
+ }
578
+
579
+ const refused: IdentityRateLimitDecision = await consume({});
580
+
581
+ expect(refused.outcome).toBe(IdentityRateLimitOutcome.RateLimited);
582
+ expect(refused.scope).toBe(IdentityRateLimitScope.Account);
583
+ });
584
+
585
+ it("keys the account counter on the address too, so it is not a lockout weapon", async () => {
586
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
587
+ await consume({ clientIp: "198.51.100.1" });
588
+ }
589
+
590
+ /*
591
+ * The same victim account, from the account owner's own address, is
592
+ * still served -- an attacker cannot burn a stranger's budget.
593
+ */
594
+ expect((await consume({ clientIp: "203.0.113.7" })).outcome).toBe(
595
+ IdentityRateLimitOutcome.Allowed,
596
+ );
597
+ });
598
+
599
+ it("separates two accounts from one address", async () => {
600
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
601
+ await consume({ accountKey: "a@example.com" });
602
+ }
603
+
604
+ expect((await consume({ accountKey: "b@example.com" })).outcome).toBe(
605
+ IdentityRateLimitOutcome.Allowed,
606
+ );
607
+ });
608
+
609
+ it("flags only the request that first crossed the line", async () => {
610
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT; i++) {
611
+ await consume({});
612
+ }
613
+
614
+ const first: IdentityRateLimitDecision = await consume({});
615
+ const second: IdentityRateLimitDecision = await consume({});
616
+
617
+ expect(first.isFirstRejectionInWindow).toBe(true);
618
+ expect(second.isFirstRejectionInWindow).toBe(false);
619
+ });
620
+
621
+ it("counts a refused request too, so hammering keeps the window pinned", async () => {
622
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 5; i++) {
623
+ await consume({});
624
+ }
625
+
626
+ const accountKeys: Array<string> = client.keysMatching(":a:");
627
+
628
+ expect(accountKeys).toHaveLength(1);
629
+ expect(client.counters.get(accountKeys[0] as string)).toBe(
630
+ PER_ACCOUNT_LIMIT + 5,
631
+ );
632
+ });
633
+ });
634
+
635
+ describe("the per-address ceiling", () => {
636
+ /*
637
+ * The email address is a request-body field, so the per-account counter
638
+ * alone is bypassed by changing it. The address counter is the ceiling
639
+ * that survives that rotation.
640
+ */
641
+ it("bounds a caller who uses a fresh email address every request", async () => {
642
+ let refusedAt: number = -1;
643
+
644
+ for (let i: number = 0; i < PER_IP_LIMIT + 1; i++) {
645
+ const decision: IdentityRateLimitDecision = await consume({
646
+ accountKey: `user-${i}@example.com`,
647
+ });
648
+
649
+ if (decision.outcome === IdentityRateLimitOutcome.RateLimited) {
650
+ refusedAt = i;
651
+ expect(decision.scope).toBe(IdentityRateLimitScope.Ip);
652
+ break;
653
+ }
654
+ }
655
+
656
+ expect(refusedAt).toBe(PER_IP_LIMIT);
657
+ });
658
+
659
+ it("does not refuse a different address once one is over its ceiling", async () => {
660
+ for (let i: number = 0; i < PER_IP_LIMIT + 1; i++) {
661
+ await consume({
662
+ accountKey: `user-${i}@example.com`,
663
+ clientIp: "198.51.100.1",
664
+ });
665
+ }
666
+
667
+ expect(
668
+ (
669
+ await consume({
670
+ accountKey: "someone@example.com",
671
+ clientIp: "203.0.113.9",
672
+ })
673
+ ).outcome,
674
+ ).toBe(IdentityRateLimitOutcome.Allowed);
675
+ });
676
+
677
+ /*
678
+ * When both counters are over, the account one is the more specific and
679
+ * the more useful thing to put in a log line.
680
+ */
681
+ it("reports the account scope first when both counters are over", async () => {
682
+ for (let i: number = 0; i < PER_IP_LIMIT + 1; i++) {
683
+ await consume({ accountKey: `user-${i}@example.com` });
684
+ }
685
+
686
+ const decision: IdentityRateLimitDecision = await consume({
687
+ accountKey: "user-0@example.com",
688
+ });
689
+
690
+ expect(decision.outcome).toBe(IdentityRateLimitOutcome.RateLimited);
691
+ expect(decision.scope).toBe(IdentityRateLimitScope.Ip);
692
+
693
+ /* Now push that one account over its own budget as well. */
694
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 2; i++) {
695
+ await consume({ accountKey: "user-0@example.com" });
696
+ }
697
+
698
+ const both: IdentityRateLimitDecision = await consume({
699
+ accountKey: "user-0@example.com",
700
+ });
701
+
702
+ expect(both.scope).toBe(IdentityRateLimitScope.Account);
703
+ });
704
+ });
705
+
706
+ describe("buckets are separate", () => {
707
+ /*
708
+ * The property that matters most: everybody at /verify-backup-code has
709
+ * just failed the factor above it. If the recovery route shared that
710
+ * counter it would answer "too many attempts" to exactly the user it
711
+ * exists to rescue.
712
+ */
713
+ it("does not let a spent two-factor budget close the recovery route", async () => {
714
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
715
+ await consume({ bucket: IdentityRateLimitBucket.TwoFactor });
716
+ }
717
+
718
+ expect(
719
+ (await consume({ bucket: IdentityRateLimitBucket.BackupCode })).outcome,
720
+ ).toBe(IdentityRateLimitOutcome.Allowed);
721
+ });
722
+
723
+ it("does not let /login noise refuse a user finishing a two-factor login", async () => {
724
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
725
+ await consume({ bucket: IdentityRateLimitBucket.Login });
726
+ }
727
+
728
+ expect(
729
+ (await consume({ bucket: IdentityRateLimitBucket.TwoFactor })).outcome,
730
+ ).toBe(IdentityRateLimitOutcome.Allowed);
731
+ });
732
+
733
+ it("gives every bucket its own key namespace", async () => {
734
+ await consume({ bucket: IdentityRateLimitBucket.Login });
735
+ await consume({ bucket: IdentityRateLimitBucket.TwoFactor });
736
+ await consume({ bucket: IdentityRateLimitBucket.BackupCode });
737
+
738
+ expect(client.keysMatching(":login:")).toHaveLength(2);
739
+ expect(client.keysMatching(":two-factor:")).toHaveLength(2);
740
+ expect(client.keysMatching(":backup-code:")).toHaveLength(2);
741
+ });
742
+ });
743
+
744
+ describe("the window", () => {
745
+ it("resets the allowance when the window rolls", async () => {
746
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
747
+ await consume({});
748
+ }
749
+
750
+ currentTime = currentTime + WINDOW_SECONDS * 1000;
751
+
752
+ expect((await consume({})).outcome).toBe(
753
+ IdentityRateLimitOutcome.Allowed,
754
+ );
755
+ });
756
+
757
+ /*
758
+ * Re-issuing EXPIRE on every increment would slide the window forward for
759
+ * as long as the attempts continue, turning a tripped limit into a
760
+ * permanent lockout.
761
+ */
762
+ it("sets the expiry only on the write that created the key", async () => {
763
+ await consume({});
764
+ await consume({});
765
+ await consume({});
766
+
767
+ const accountKey: string = client.keysMatching(":a:")[0] as string;
768
+
769
+ expect(client.expiresForKey(accountKey)).toHaveLength(1);
770
+ });
771
+
772
+ it("outlives its window so a boundary request cannot read a reclaimed key", async () => {
773
+ await consume({});
774
+
775
+ const accountKey: string = client.keysMatching(":a:")[0] as string;
776
+ const recorded: RecordedExpire = client.expiresForKey(
777
+ accountKey,
778
+ )[0] as RecordedExpire;
779
+
780
+ expect(recorded.ttlSeconds).toBe(WINDOW_SECONDS * TTL_MULTIPLIER);
781
+ });
782
+
783
+ it("tells a refused caller how long is left in the current window", async () => {
784
+ /* A quarter of the way into the window. */
785
+ currentTime = currentTime + (WINDOW_SECONDS * 1000) / 4;
786
+
787
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT; i++) {
788
+ await consume({});
789
+ }
790
+
791
+ const decision: IdentityRateLimitDecision = await consume({});
792
+
793
+ expect(decision.retryAfterSeconds).toBe((WINDOW_SECONDS * 3) / 4);
794
+ });
795
+
796
+ it("never tells a caller to retry in zero seconds", async () => {
797
+ /* One millisecond before the window rolls. */
798
+ currentTime = currentTime + WINDOW_SECONDS * 1000 - 1;
799
+
800
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT; i++) {
801
+ await consume({});
802
+ }
803
+
804
+ const decision: IdentityRateLimitDecision = await consume({});
805
+
806
+ expect(decision.retryAfterSeconds).toBeGreaterThanOrEqual(1);
807
+ });
808
+
809
+ it("costs one round trip on the common allowed path", async () => {
810
+ /* First request creates both keys, so it also issues the EXPIRE pass. */
811
+ await consume({});
812
+ client.pipelineCallCount = 0;
813
+
814
+ await consume({});
815
+
816
+ expect(client.pipelineCallCount).toBe(1);
817
+ });
818
+ });
819
+
820
+ describe("when the counter is unavailable", () => {
821
+ /*
822
+ * The opposite of VerificationCodeRateLimit. Nothing else on these routes
823
+ * counts attempts, so serving them without the counter means serving the
824
+ * unlimited guessing oracle the middleware exists to close.
825
+ */
826
+ it("reports unavailable when Redis has no client", async () => {
827
+ getClientMock.mockReturnValue(null);
828
+
829
+ expect((await consume({})).outcome).toBe(
830
+ IdentityRateLimitOutcome.CounterUnavailable,
831
+ );
832
+ });
833
+
834
+ it("reports unavailable when Redis is disconnected", async () => {
835
+ isConnectedMock.mockReturnValue(false);
836
+
837
+ expect((await consume({})).outcome).toBe(
838
+ IdentityRateLimitOutcome.CounterUnavailable,
839
+ );
840
+ });
841
+
842
+ it("reports unavailable when the pipeline rejects", async () => {
843
+ client.failNextExec = new Error("boom");
844
+
845
+ expect((await consume({})).outcome).toBe(
846
+ IdentityRateLimitOutcome.CounterUnavailable,
847
+ );
848
+ });
849
+
850
+ it("reports unavailable when the pipeline returns nothing", async () => {
851
+ client.malformedExecResult = null;
852
+
853
+ expect((await consume({})).outcome).toBe(
854
+ IdentityRateLimitOutcome.CounterUnavailable,
855
+ );
856
+ });
857
+
858
+ it("reports unavailable when the pipeline returns too few results", async () => {
859
+ client.malformedExecResult = [[null, 1]];
860
+
861
+ expect((await consume({})).outcome).toBe(
862
+ IdentityRateLimitOutcome.CounterUnavailable,
863
+ );
864
+ });
865
+
866
+ it("reports unavailable when a counter carries an error", async () => {
867
+ client.malformedExecResult = [
868
+ [new Error("WRONGTYPE"), null],
869
+ [null, 1],
870
+ ];
871
+
872
+ expect((await consume({})).outcome).toBe(
873
+ IdentityRateLimitOutcome.CounterUnavailable,
874
+ );
875
+ });
876
+
877
+ it("reports unavailable when a counter is not numeric", async () => {
878
+ client.malformedExecResult = [
879
+ [null, "1"],
880
+ [null, 1],
881
+ ];
882
+
883
+ expect((await consume({})).outcome).toBe(
884
+ IdentityRateLimitOutcome.CounterUnavailable,
885
+ );
886
+ });
887
+
888
+ it("does not throw out of consume on any failure", async () => {
889
+ client.failNextExec = new Error("boom");
890
+
891
+ await expect(consume({})).resolves.toBeDefined();
892
+ });
893
+ });
894
+
895
+ describe("getMiddleware", () => {
896
+ const runMiddleware: (data: {
897
+ bucket?: IdentityRateLimitBucket;
898
+ request?: ExpressRequest;
899
+ }) => Promise<{
900
+ nextCalled: boolean;
901
+ headers: Record<string, string>;
902
+ }> = async (data: {
903
+ bucket?: IdentityRateLimitBucket;
904
+ request?: ExpressRequest;
905
+ }) => {
906
+ const { response, headers } = buildResponse();
907
+ let nextCalled: boolean = false;
908
+
909
+ const next: NextFunction = (() => {
910
+ nextCalled = true;
911
+ }) as unknown as NextFunction;
912
+
913
+ await IdentityRateLimit.getMiddleware(
914
+ data.bucket || IdentityRateLimitBucket.Login,
915
+ )(
916
+ data.request ||
917
+ buildRequest({
918
+ body: { data: { email: "user@example.com" } },
919
+ headers: { "x-forwarded-for": "203.0.113.7" },
920
+ }),
921
+ response,
922
+ next,
923
+ );
924
+
925
+ return { nextCalled, headers };
926
+ };
927
+
928
+ it("passes an allowed request through", async () => {
929
+ const result: { nextCalled: boolean } = await runMiddleware({});
930
+
931
+ expect(result.nextCalled).toBe(true);
932
+ expect(sendErrorResponseMock).not.toHaveBeenCalled();
933
+ });
934
+
935
+ /*
936
+ * The property the whole middleware rests on: a refused request must not
937
+ * reach the handler, or the user lookup and the bcrypt verify behind it
938
+ * still happen and the limiter is decoration.
939
+ */
940
+ it("stops a refused request from reaching the route handler", async () => {
941
+ const request: ExpressRequest = buildRequest({
942
+ body: { data: { email: "user@example.com" } },
943
+ headers: { "x-forwarded-for": "203.0.113.7" },
944
+ });
945
+
946
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT; i++) {
947
+ await runMiddleware({ request });
948
+ }
949
+
950
+ sendErrorResponseMock.mockClear();
951
+
952
+ const result: { nextCalled: boolean } = await runMiddleware({ request });
953
+
954
+ expect(result.nextCalled).toBe(false);
955
+ expect(sendErrorResponseMock).toHaveBeenCalledTimes(1);
956
+ });
957
+
958
+ it("answers a refusal with 429", async () => {
959
+ const request: ExpressRequest = buildRequest({
960
+ body: { data: { email: "user@example.com" } },
961
+ headers: { "x-forwarded-for": "203.0.113.7" },
962
+ });
963
+
964
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
965
+ await runMiddleware({ request });
966
+ }
967
+
968
+ const error: Exception = sendErrorResponseMock.mock
969
+ .calls[0]?.[2] as Exception;
970
+
971
+ expect(error.code).toBe(ExceptionCode.TooManyRequestsException);
972
+ });
973
+
974
+ it("sets Retry-After on a refusal", async () => {
975
+ const request: ExpressRequest = buildRequest({
976
+ body: { data: { email: "user@example.com" } },
977
+ headers: { "x-forwarded-for": "203.0.113.7" },
978
+ });
979
+
980
+ let headers: Record<string, string> = {};
981
+
982
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
983
+ headers = (await runMiddleware({ request })).headers;
984
+ }
985
+
986
+ expect(Number(headers["Retry-After"])).toBeGreaterThan(0);
987
+ });
988
+
989
+ /*
990
+ * These routes are careful not to say which half of a credential was
991
+ * wrong. A limiter that answered differently for a real address than an
992
+ * invented one would hand back the enumeration the handlers withhold.
993
+ */
994
+ it("says the same thing whichever counter fired", async () => {
995
+ const accountRefused: ExpressRequest = buildRequest({
996
+ body: { data: { email: "user@example.com" } },
997
+ headers: { "x-forwarded-for": "203.0.113.7" },
998
+ });
999
+
1000
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
1001
+ await runMiddleware({ request: accountRefused });
1002
+ }
1003
+
1004
+ const accountMessage: string = (
1005
+ sendErrorResponseMock.mock.calls[0]?.[2] as Exception
1006
+ ).message;
1007
+
1008
+ sendErrorResponseMock.mockClear();
1009
+ client.reset();
1010
+
1011
+ for (let i: number = 0; i < PER_IP_LIMIT + 1; i++) {
1012
+ await runMiddleware({
1013
+ request: buildRequest({
1014
+ body: { data: { email: `user-${i}@example.com` } },
1015
+ headers: { "x-forwarded-for": "203.0.113.7" },
1016
+ }),
1017
+ });
1018
+ }
1019
+
1020
+ const ipMessage: string = (
1021
+ sendErrorResponseMock.mock.calls[0]?.[2] as Exception
1022
+ ).message;
1023
+
1024
+ expect(accountMessage).toBe(ipMessage);
1025
+ });
1026
+
1027
+ /*
1028
+ * Fails closed. A 503 rather than an unthrottled route: logging in is
1029
+ * already impossible with Redis down, so this costs no availability that
1030
+ * was not already lost.
1031
+ */
1032
+ it("refuses with 503 rather than running unthrottled", async () => {
1033
+ isConnectedMock.mockReturnValue(false);
1034
+
1035
+ const result: { nextCalled: boolean } = await runMiddleware({});
1036
+
1037
+ expect(result.nextCalled).toBe(false);
1038
+
1039
+ const error: Exception = sendErrorResponseMock.mock
1040
+ .calls[0]?.[2] as Exception;
1041
+
1042
+ expect(error.code).toBe(ExceptionCode.ServiceUnavailableException);
1043
+ });
1044
+
1045
+ it("logs the crossing once rather than every refusal", async () => {
1046
+ const request: ExpressRequest = buildRequest({
1047
+ body: { data: { email: "user@example.com" } },
1048
+ headers: { "x-forwarded-for": "203.0.113.7" },
1049
+ });
1050
+
1051
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT; i++) {
1052
+ await runMiddleware({ request });
1053
+ }
1054
+
1055
+ loggerWarnMock.mockClear();
1056
+
1057
+ await runMiddleware({ request });
1058
+ await runMiddleware({ request });
1059
+ await runMiddleware({ request });
1060
+
1061
+ expect(loggerWarnMock).toHaveBeenCalledTimes(1);
1062
+ });
1063
+
1064
+ /*
1065
+ * A Redis outage puts every request on the unavailable path, so an
1066
+ * unguarded log line there is one per request for as long as it lasts.
1067
+ */
1068
+ it("throttles the counter-unavailable log", async () => {
1069
+ isConnectedMock.mockReturnValue(false);
1070
+ loggerErrorMock.mockClear();
1071
+
1072
+ /* Move well past any interval a previous test may have consumed. */
1073
+ currentTime = currentTime + 10 * 60 * 1000;
1074
+
1075
+ await runMiddleware({ bucket: IdentityRateLimitBucket.BackupCode });
1076
+ await runMiddleware({ bucket: IdentityRateLimitBucket.BackupCode });
1077
+ await runMiddleware({ bucket: IdentityRateLimitBucket.BackupCode });
1078
+
1079
+ expect(loggerErrorMock).toHaveBeenCalledTimes(1);
1080
+ });
1081
+
1082
+ it("logs the counter-unavailable condition again once the interval passes", async () => {
1083
+ isConnectedMock.mockReturnValue(false);
1084
+ loggerErrorMock.mockClear();
1085
+
1086
+ currentTime = currentTime + 10 * 60 * 1000;
1087
+ await runMiddleware({ bucket: IdentityRateLimitBucket.TwoFactor });
1088
+
1089
+ currentTime = currentTime + 61 * 1000;
1090
+ await runMiddleware({ bucket: IdentityRateLimitBucket.TwoFactor });
1091
+
1092
+ expect(loggerErrorMock).toHaveBeenCalledTimes(2);
1093
+ });
1094
+
1095
+ it("survives a response with no setHeader", async () => {
1096
+ const request: ExpressRequest = buildRequest({
1097
+ body: { data: { email: "user@example.com" } },
1098
+ headers: { "x-forwarded-for": "203.0.113.7" },
1099
+ });
1100
+
1101
+ for (let i: number = 0; i < PER_ACCOUNT_LIMIT + 1; i++) {
1102
+ await consume({
1103
+ accountKey: "user@example.com",
1104
+ clientIp: "203.0.113.7",
1105
+ });
1106
+ }
1107
+
1108
+ let nextCalled: boolean = false;
1109
+
1110
+ await expect(
1111
+ IdentityRateLimit.getMiddleware(IdentityRateLimitBucket.Login)(
1112
+ request,
1113
+ {} as unknown as ExpressResponse,
1114
+ (() => {
1115
+ nextCalled = true;
1116
+ }) as unknown as NextFunction,
1117
+ ),
1118
+ ).resolves.toBeUndefined();
1119
+
1120
+ expect(nextCalled).toBe(false);
1121
+ });
1122
+
1123
+ /*
1124
+ * The middleware and consume() must agree on the key, or the middleware
1125
+ * counts in a bucket nothing else reads.
1126
+ */
1127
+ it("counts the middleware request into the same bucket consume uses", async () => {
1128
+ await runMiddleware({});
1129
+
1130
+ const decision: IdentityRateLimitDecision = await consume({
1131
+ accountKey: "user@example.com",
1132
+ clientIp: "203.0.113.7",
1133
+ bucket: IdentityRateLimitBucket.Login,
1134
+ });
1135
+
1136
+ expect(decision.outcome).toBe(IdentityRateLimitOutcome.Allowed);
1137
+
1138
+ const accountKeys: Array<string> = client.keysMatching(":login:a:");
1139
+
1140
+ expect(accountKeys).toHaveLength(1);
1141
+ expect(client.counters.get(accountKeys[0] as string)).toBe(2);
1142
+ });
1143
+ });
1144
+ });