@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.
- package/Models/AnalyticsModels/SecurityEvent.ts +48 -16
- package/Models/AnalyticsModels/ThreatIntelIndicator.ts +10 -5
- package/Models/DatabaseModels/AlertSeverity.ts +44 -0
- package/Models/DatabaseModels/AlertState.ts +0 -2
- package/Models/DatabaseModels/DetectionRule.ts +16 -5
- package/Models/DatabaseModels/Domain.ts +1 -1
- package/Models/DatabaseModels/GoogleSecOpsConnection.ts +14 -5
- package/Models/DatabaseModels/Incident.ts +1 -1
- package/Models/DatabaseModels/IncidentSeverity.ts +44 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/MetricType.ts +0 -2
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
- package/Models/DatabaseModels/MonitorTest.ts +0 -2
- package/Models/DatabaseModels/NetworkDevice.ts +234 -5
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +88 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +22 -0
- package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +501 -0
- package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
- package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
- package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
- package/Models/DatabaseModels/StatusPageResource.ts +0 -2
- package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
- package/Models/DatabaseModels/ThreatIntelFeed.ts +16 -5
- package/Server/API/GlobalConfigAPI.ts +362 -183
- package/Server/API/TelemetryAPI.ts +42 -4
- package/Server/EnvironmentConfig.ts +112 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/MultipartFormData.ts +118 -32
- package/Server/Services/AlertEpisodeLabelRuleEngineService.ts +9 -1
- package/Server/Services/AlertEpisodeOnCallRuleEngineService.ts +9 -1
- package/Server/Services/AlertEpisodeOwnerRuleEngineService.ts +9 -1
- package/Server/Services/AlertEpisodePrivacyRuleEngineService.ts +9 -1
- package/Server/Services/AlertGroupingEngineService.ts +9 -1
- package/Server/Services/AlertLabelRuleEngineService.ts +9 -1
- package/Server/Services/AlertOnCallRuleEngineService.ts +9 -1
- package/Server/Services/AlertOwnerRuleEngineService.ts +9 -1
- package/Server/Services/AlertPrivacyRuleEngineService.ts +9 -1
- package/Server/Services/AlertReminderRuleService.ts +9 -1
- package/Server/Services/AutoRemediationRuleEngineService.ts +9 -1
- package/Server/Services/CephClusterLabelRuleEngineService.ts +9 -1
- package/Server/Services/CephClusterOwnerRuleEngineService.ts +9 -1
- package/Server/Services/CloudResourceLabelRuleEngineService.ts +9 -1
- package/Server/Services/CloudResourceOwnerRuleEngineService.ts +9 -1
- package/Server/Services/DashboardLabelRuleEngineService.ts +9 -1
- package/Server/Services/DashboardOwnerRuleEngineService.ts +9 -1
- package/Server/Services/DatabaseService.ts +39 -9
- package/Server/Services/DockerHostLabelRuleEngineService.ts +9 -1
- package/Server/Services/DockerHostOwnerRuleEngineService.ts +9 -1
- package/Server/Services/DockerSwarmClusterLabelRuleEngineService.ts +9 -1
- package/Server/Services/DockerSwarmClusterOwnerRuleEngineService.ts +9 -1
- package/Server/Services/HostLabelRuleEngineService.ts +9 -1
- package/Server/Services/HostOwnerRuleEngineService.ts +9 -1
- package/Server/Services/IncidentEpisodeLabelRuleEngineService.ts +9 -1
- package/Server/Services/IncidentEpisodeOnCallRuleEngineService.ts +9 -1
- package/Server/Services/IncidentEpisodeOwnerRuleEngineService.ts +9 -1
- package/Server/Services/IncidentEpisodePrivacyRuleEngineService.ts +9 -1
- package/Server/Services/IncidentGroupingEngineService.ts +9 -1
- package/Server/Services/IncidentLabelRuleEngineService.ts +9 -1
- package/Server/Services/IncidentOnCallRuleEngineService.ts +9 -1
- package/Server/Services/IncidentOwnerRuleEngineService.ts +9 -1
- package/Server/Services/IncidentPrivacyRuleEngineService.ts +9 -1
- package/Server/Services/IncidentReminderRuleService.ts +9 -1
- package/Server/Services/IncidentSlaRuleService.ts +9 -1
- package/Server/Services/IncomingCallPolicyLabelRuleEngineService.ts +9 -1
- package/Server/Services/IncomingCallPolicyOwnerRuleEngineService.ts +9 -1
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/IoTFleetLabelRuleEngineService.ts +9 -1
- package/Server/Services/IoTFleetOwnerRuleEngineService.ts +9 -1
- package/Server/Services/KubernetesClusterLabelRuleEngineService.ts +9 -1
- package/Server/Services/KubernetesClusterOwnerRuleEngineService.ts +9 -1
- package/Server/Services/KubernetesResourceService.ts +5 -1
- package/Server/Services/MonitorLabelRuleEngineService.ts +9 -1
- package/Server/Services/MonitorOwnerRuleEngineService.ts +9 -1
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +35 -0
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +96 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +9 -1
- package/Server/Services/NetworkDeviceOidTemplateService.ts +201 -0
- package/Server/Services/NetworkDeviceOwnerRuleEngineService.ts +9 -1
- package/Server/Services/NetworkDeviceRoleService.ts +90 -0
- package/Server/Services/NetworkDeviceService.ts +209 -1
- package/Server/Services/OnCallDutyPolicyLabelRuleEngineService.ts +9 -1
- package/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.ts +9 -1
- package/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.ts +9 -1
- package/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.ts +9 -1
- package/Server/Services/PodmanHostLabelRuleEngineService.ts +9 -1
- package/Server/Services/PodmanHostOwnerRuleEngineService.ts +9 -1
- package/Server/Services/ProjectService.ts +85 -0
- package/Server/Services/ProxmoxClusterLabelRuleEngineService.ts +9 -1
- package/Server/Services/ProxmoxClusterOwnerRuleEngineService.ts +9 -1
- package/Server/Services/RumApplicationLabelRuleEngineService.ts +9 -1
- package/Server/Services/RumApplicationOwnerRuleEngineService.ts +9 -1
- package/Server/Services/RunbookLabelRuleEngineService.ts +9 -1
- package/Server/Services/RunbookOwnerRuleEngineService.ts +9 -1
- package/Server/Services/RunbookRuleEngineService.ts +9 -1
- package/Server/Services/ScheduledMaintenanceLabelRuleEngineService.ts +9 -1
- package/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.ts +9 -1
- package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +9 -1
- package/Server/Services/ServerlessFunctionLabelRuleEngineService.ts +9 -1
- package/Server/Services/ServerlessFunctionOwnerRuleEngineService.ts +9 -1
- package/Server/Services/ServiceLabelRuleEngineService.ts +9 -1
- package/Server/Services/ServiceOwnerRuleEngineService.ts +9 -1
- package/Server/Services/StatusPageLabelRuleEngineService.ts +9 -1
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +7 -0
- package/Server/Services/StatusPageOwnerRuleEngineService.ts +9 -1
- package/Server/Services/TelemetrySourceMapService.ts +266 -23
- package/Server/Services/UserNotificationSettingService.ts +0 -6
- package/Server/Services/UserService.ts +27 -0
- package/Server/Services/WorkflowLabelRuleEngineService.ts +9 -1
- package/Server/Services/WorkflowOwnerRuleEngineService.ts +9 -1
- package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +186 -0
- package/Server/Utils/Monitor/CapturedMetricAttributeUtil.ts +245 -0
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +21 -2
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +55 -21
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +32 -0
- package/Server/Utils/Monitor/NetworkDeviceMetricUtil.ts +15 -2
- package/Server/Utils/Monitor/NetworkDeviceWalkUtil.ts +18 -1
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +10 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +34 -0
- package/Server/Utils/Rules/RuleEngineRuleRead.ts +48 -0
- package/Server/Utils/Telemetry/ExceptionSanitizer.ts +14 -1
- package/Server/Utils/Telemetry/LogExceptionExtractor.ts +14 -2
- package/Server/Utils/Telemetry/SourceMapResolver.ts +16 -5
- package/Server/Utils/Telemetry/StackTraceParser.ts +399 -0
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
- package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +125 -0
- package/Tests/App/Dashboard/HeaderImageAssets.test.tsx +93 -0
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
- package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
- package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3 -3
- package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +12 -4
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +1 -1
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +14 -0
- package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +543 -0
- package/Tests/Models/SecurityDomainAccessControl.test.ts +429 -0
- package/Tests/Server/API/GlobalConfigLicense.test.ts +657 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +84 -6
- package/Tests/Server/API/TelemetryResolveStackTraceAPI.test.ts +805 -0
- package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +32 -7
- package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +1144 -0
- package/Tests/Server/Middleware/MultipartFormData.test.ts +618 -3
- package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
- package/Tests/Server/Services/MonitorLabelRuleEngineScale.test.ts +520 -0
- package/Tests/Server/Services/NetworkDeviceOidTemplateService.test.ts +332 -0
- package/Tests/Server/Services/NetworkDeviceOidTemplateWriteGuards.test.ts +409 -0
- package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
- package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
- package/Tests/Server/Services/RuleEngineRuleFetchLimit.test.ts +239 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
- package/Tests/Server/Services/TelemetrySourceMapService.test.ts +1210 -2
- package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
- package/Tests/Server/Services/UserServiceSeatLimit.test.ts +182 -0
- package/Tests/Server/Types/AnalyticsDatabase/SecurityEventModelPermission.test.ts +255 -0
- package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +212 -0
- package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +413 -0
- package/Tests/Server/Utils/BasicCron.test.ts +283 -0
- package/Tests/Server/Utils/CodeRepository.test.ts +689 -0
- package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +495 -0
- package/Tests/Server/Utils/ExceptionSanitizer.test.ts +77 -0
- package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
- package/Tests/Server/Utils/Monitor/CapturedMetricAttributeUtil.test.ts +284 -0
- package/Tests/Server/Utils/Monitor/MonitorCustomMetricAttributes.test.ts +503 -0
- package/Tests/Server/Utils/Monitor/MonitorTemplatePrototypePollution.test.ts +106 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkLogWritePath.test.ts +33 -4
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +14 -4
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +44 -0
- package/Tests/Server/Utils/OpenAPISpec.test.ts +536 -0
- package/Tests/Server/Utils/Rules/RuleEngineRuleRead.test.ts +115 -0
- package/Tests/Server/Utils/StackTraceParser.test.ts +610 -0
- package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
- package/Tests/Server/Utils/Telemetry/SourceMapLimits.test.ts +656 -0
- package/Tests/Server/Utils/Telemetry/SourceMapResolver.test.ts +100 -0
- package/Tests/Types/Monitor/SnmpMonitor/SnmpOidListUtil.test.ts +454 -0
- package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
- package/Tests/Types/SecurityRolePermissions.test.ts +193 -0
- package/Tests/UI/Components/Charts/AnnotationLayout.test.ts +773 -0
- package/Tests/UI/Components/Charts/ChartAnnotationRail.test.tsx +789 -0
- package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +254 -0
- package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
- package/Tests/UI/Components/Charts/TimeAnnotation.test.ts +471 -0
- package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +405 -0
- package/Tests/UI/Components/ErrorMessageRefreshControl.test.tsx +83 -0
- package/Tests/UI/Components/MarkdownConverters.test.ts +686 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
- package/Tests/UI/Components/ModelTable/BaseModelTableDeleteAndEmptyStates.test.tsx +341 -0
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
- package/Tests/UI/Components/OrderedStatesList.test.tsx +5 -5
- package/Tests/UI/Components/PendingProjectInvitations.test.tsx +1 -1
- package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
- package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +195 -0
- package/Tests/UI/Components/XAxis.test.ts +270 -5
- package/Tests/UI/Components/XAxisDaylightSaving.test.ts +113 -0
- package/Tests/UI/Styles/ChartFocusRing.test.ts +153 -0
- package/Tests/UI/Styles/ChartFocusRingCoverage.test.tsx +377 -0
- package/Tests/UI/Styles/ThemeStylesheet.ts +162 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
- package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
- package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
- package/Tests/Utils/EnterpriseLicenseSeats.test.ts +507 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
- package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
- package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +951 -1
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +238 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +1056 -0
- package/Tests/Utils/NetworkDiscovery/ReverseDnsNameUtil.test.ts +882 -0
- package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
- package/Tests/Utils/Slug.test.ts +61 -1
- package/Tests/jest.setup.ts +19 -0
- package/Types/Date.ts +39 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
- package/Types/Monitor/SnmpMonitor/SnmpOidListUtil.ts +333 -0
- package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
- package/Types/Permission.ts +141 -0
- package/Types/Rum/SessionReplay.ts +21 -0
- package/Types/Telemetry/SourceMap.ts +8 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
- package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
- package/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.tsx +176 -0
- package/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.ts +745 -0
- package/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.tsx +576 -0
- package/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.tsx +204 -0
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +554 -545
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +337 -371
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +549 -540
- package/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.ts +7 -3
- package/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.ts +14 -3
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +7 -0
- package/UI/Components/Charts/Types/ChartEventKind.ts +13 -0
- package/UI/Components/Charts/Types/ReferenceRegionProps.ts +6 -0
- package/UI/Components/Charts/Types/TimeReferenceLineProps.ts +20 -3
- package/UI/Components/Charts/Utils/TimeAnnotation.ts +69 -35
- package/UI/Components/Charts/Utils/XAxis.ts +125 -18
- package/UI/Components/EditionLabel/EditionLabel.tsx +280 -26
- package/UI/Components/ErrorMessage/ErrorMessage.tsx +21 -10
- package/UI/Components/Feed/FeedItem.tsx +4 -5
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -2
- package/UI/Components/Header/UserProfile/UserProfile.tsx +1 -2
- package/UI/Components/Image/Image.tsx +20 -3
- package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
- package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +1 -2
- package/UI/Components/ModelTable/BaseModelTable.tsx +200 -39
- package/UI/Components/Navbar/NavBar.tsx +62 -13
- package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
- package/UI/Components/SideMenu/SideMenu.tsx +12 -2
- package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
- package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
- package/UI/Components/Table/TableHeader.tsx +64 -48
- package/UI/Components/Table/TableRow.tsx +13 -3
- package/UI/Styles/Theme.css +38 -0
- package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +271 -0
- package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
- package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
- package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +6 -0
- package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +141 -10
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +55 -3
- package/Utils/NetworkDiscovery/ReverseDnsNameUtil.ts +151 -0
- package/Utils/Rules/RuleEngineLimits.ts +32 -0
- package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
- package/Utils/Slug.ts +34 -1
- package/build/dist/Models/AnalyticsModels/SecurityEvent.js +48 -16
- package/build/dist/Models/AnalyticsModels/SecurityEvent.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js +10 -5
- package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertSeverity.js +46 -1
- package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
- package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DetectionRule.js +16 -5
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +14 -5
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js +46 -1
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
- package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +239 -6
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +88 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +521 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js +16 -5
- package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +293 -176
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +31 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +83 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MultipartFormData.js +93 -28
- package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertGroupingEngineService.js +8 -1
- package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertOnCallRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertOnCallRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js +8 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DashboardLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DashboardLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +31 -5
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/HostLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/HostLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/HostOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/HostOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentReminderRuleService.js +8 -1
- package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSlaRuleService.js +8 -1
- package/build/dist/Server/Services/IncidentSlaRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +5 -1
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/MonitorLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/MonitorLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +30 -21
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +64 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceOidTemplateService.js +180 -0
- package/build/dist/Server/Services/NetworkDeviceOidTemplateService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
- package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceService.js +154 -1
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +65 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RunbookLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RunbookLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RunbookRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RunbookRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +8 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ServiceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +6 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/TelemetrySourceMapService.js +213 -19
- package/build/dist/Server/Services/TelemetrySourceMapService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +25 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +145 -0
- package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/CapturedMetricAttributeUtil.js +201 -0
- package/build/dist/Server/Utils/Monitor/CapturedMetricAttributeUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +19 -2
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +42 -23
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +29 -0
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js +11 -2
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js +16 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +10 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +33 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Rules/RuleEngineRuleRead.js +37 -0
- package/build/dist/Server/Utils/Rules/RuleEngineRuleRead.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +12 -1
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +14 -2
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js +15 -5
- package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +307 -0
- package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -1
- package/build/dist/Types/Date.js +27 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpOidListUtil.js +253 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpOidListUtil.js.map +1 -0
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
- package/build/dist/Types/Permission.js +126 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +19 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.js +83 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.js +531 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.js +264 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.js +122 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +262 -234
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +146 -153
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +261 -233
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Types/ChartEventKind.js +14 -0
- package/build/dist/UI/Components/Charts/Types/ChartEventKind.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +57 -28
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +103 -17
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +198 -23
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js +15 -5
- package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js +4 -5
- package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -2
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Header/UserProfile/UserProfile.js.map +1 -1
- package/build/dist/UI/Components/Image/Image.js +6 -1
- package/build/dist/UI/Components/Image/Image.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +1 -2
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +143 -36
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +20 -22
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +13 -4
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +153 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +127 -7
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +50 -3
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/ReverseDnsNameUtil.js +132 -0
- package/build/dist/Utils/NetworkDiscovery/ReverseDnsNameUtil.js.map +1 -0
- package/build/dist/Utils/Rules/RuleEngineLimits.js +31 -0
- package/build/dist/Utils/Rules/RuleEngineLimits.js.map +1 -0
- package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
- package/build/dist/Utils/Slug.js +31 -1
- package/build/dist/Utils/Slug.js.map +1 -1
- package/jest.config.json +1 -0
- 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
|
+
});
|