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