@oneuptime/common 12.0.22 → 12.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/DetectionRule.ts +55 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +584 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +105 -0
- package/Models/DatabaseModels/Project.ts +68 -0
- package/Models/DatabaseModels/User.ts +68 -0
- package/Server/API/TelemetryAPI.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
- package/Server/Middleware/GzipRequestBody.ts +311 -0
- package/Server/Middleware/MultipartFormData.ts +87 -3
- package/Server/Services/DetectionRuleService.ts +73 -6
- package/Server/Services/Index.ts +207 -0
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
- package/Server/Services/NetworkDeviceService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
- package/Server/Services/ProjectService.ts +29 -19
- package/Server/Services/SecurityEventService.ts +90 -13
- package/Server/Services/StatusPageResourceService.ts +223 -0
- package/Server/Services/TelemetryAttributeService.ts +17 -0
- package/Server/Services/UserService.ts +4 -6
- package/Server/Types/Markdown.ts +161 -67
- package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
- package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
- package/Server/Utils/Attribution.ts +4 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
- package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
- package/Server/Utils/StartServer.ts +12 -32
- package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
- package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
- package/Server/Views/Partials/Head.ejs +2 -1
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
- package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
- package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
- package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
- package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
- package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
- package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
- package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
- package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
- package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
- package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
- package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
- package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
- package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
- package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
- package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
- package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
- package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
- package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
- package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
- package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
- package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
- package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
- package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
- package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
- package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
- package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
- package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
- package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
- package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
- package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
- package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
- package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
- package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
- package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
- package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
- package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
- package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
- package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
- package/Tests/UI/Components/SideOver.test.tsx +1 -1
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
- package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
- package/Tests/UI/Components/Toggle.test.tsx +25 -0
- package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
- package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
- package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
- package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
- package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
- package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
- package/Types/Code/YamlSyntax.ts +396 -0
- package/Types/Exception/ExceptionCode.ts +1 -0
- package/Types/Exception/PayloadTooLargeException.ts +8 -0
- package/Types/Marketing/Attribution.ts +10 -0
- package/Types/Marketing/MarketingEvent.ts +4 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
- package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
- package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
- package/Types/Permission.ts +47 -0
- package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
- package/Types/Telemetry/LlmMetricConventions.ts +124 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
- package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
- package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
- package/UI/Components/Detail/Detail.tsx +10 -0
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
- package/UI/Components/Forms/Fields/FormField.tsx +31 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
- package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
- package/UI/Components/Forms/Validation.ts +46 -0
- package/UI/Components/Input/Input.tsx +3 -0
- package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
- package/UI/Components/ModelTable/Column.ts +8 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
- package/UI/Components/SideOver/SideOver.tsx +5 -5
- package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
- package/UI/Components/StatusPage/ResourceList.tsx +197 -38
- package/UI/Components/Toggle/Toggle.tsx +9 -1
- package/UI/Components/Types/FieldType.ts +1 -0
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
- package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
- package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
- package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
- package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
- package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
- package/Utils/StatusPage/ResourceExplorer.ts +258 -0
- package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +609 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +97 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +76 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +76 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +17 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
- package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
- package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
- package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
- package/build/dist/Server/Services/DetectionRuleService.js +49 -4
- package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +206 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -45
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventService.js +57 -5
- package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
- package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +2 -12
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +147 -61
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -21
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Types/Code/YamlSyntax.js +212 -0
- package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
- package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
- package/build/dist/Types/Marketing/Attribution.js +10 -0
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/Permission.js +41 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
- package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +9 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +27 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
- package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Types/FieldType.js +1 -0
- package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
- package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/Tests/UI/Components/Toast.test.tsx +0 -197
- package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
- package/UI/Components/Toast/Toast.tsx +0 -189
- package/UI/Components/Toast/ToastInit.tsx +0 -110
- package/build/dist/UI/Components/Toast/Toast.js +0 -123
- package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
- package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Contract under test — the network device auto-import rule engine (issue
|
|
3
|
+
* #3378): the automatic path that consumes one completed discovery scan
|
|
4
|
+
* (processCompletedScan) and the manual "Run Now" / dry run
|
|
5
|
+
* (applyRuleToCompletedScans).
|
|
6
|
+
*
|
|
7
|
+
* What these tests pin is the bookkeeping that makes automatic imports safe
|
|
8
|
+
* to run every worker tick, because each piece fails silently on its own:
|
|
9
|
+
*
|
|
10
|
+
* - the autoImportProcessedAt marker protocol: a stamped scan is never
|
|
11
|
+
* re-processed, every stamp is a compare-and-set on (status,
|
|
12
|
+
* completedAt), and a truncated pass that made PROGRESS leaves the
|
|
13
|
+
* marker NULL so the next tick resumes — while a truncated pass that
|
|
14
|
+
* created nothing stamps anyway, or the sweep would repeat the same
|
|
15
|
+
* doomed pass forever;
|
|
16
|
+
* - the sweep lock: a real Run Now holds the same Redis lock as the
|
|
17
|
+
* worker sweep (the check-then-create idempotency has no DB backstop),
|
|
18
|
+
* dry runs never touch it, and the lock is released even when the run
|
|
19
|
+
* body throws;
|
|
20
|
+
* - idempotency: an address already in the inventory is skipped, and a
|
|
21
|
+
* create that loses a race reads as "already registered", never as a
|
|
22
|
+
* renamed twin;
|
|
23
|
+
* - the name-collision retry: same device, fallback name, exactly once;
|
|
24
|
+
* - the jsonb write-back: only the created rows flip isAlreadyRegistered,
|
|
25
|
+
* everything else — junk rows included — passes through verbatim;
|
|
26
|
+
* - staleness: results older than MAX_RESULT_AGE_IN_HOURS are retired
|
|
27
|
+
* without importing (Run Now stays the deliberate path to old results);
|
|
28
|
+
* - a dry run evaluates everything, writes nothing, and is bounded by the
|
|
29
|
+
* SAME attempt budget as the real run it predicts.
|
|
30
|
+
*
|
|
31
|
+
* The collaborating singleton services — and the Semaphore module, which
|
|
32
|
+
* would otherwise reach Redis — are stubbed at the MODULE level before the
|
|
33
|
+
* engine is imported: their real files reach Postgres through
|
|
34
|
+
* DatabaseService (and PasswordHash, the local-only ts-jest compile
|
|
35
|
+
* failure), and nothing here should touch any of it. Pure in-memory rows.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
jest.mock("../../../Server/Services/NetworkDeviceService", () => {
|
|
39
|
+
return {
|
|
40
|
+
__esModule: true,
|
|
41
|
+
default: {
|
|
42
|
+
create: jest.fn(),
|
|
43
|
+
findBy: jest.fn(),
|
|
44
|
+
findOneBy: jest.fn(),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
jest.mock("../../../Server/Services/NetworkDeviceDiscoveryScanService", () => {
|
|
50
|
+
return {
|
|
51
|
+
__esModule: true,
|
|
52
|
+
default: {
|
|
53
|
+
findOneBy: jest.fn(),
|
|
54
|
+
findBy: jest.fn(),
|
|
55
|
+
updateColumnsByIdWithoutHooks: jest.fn(),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
jest.mock("../../../Server/Services/NetworkDeviceAutoImportRuleService", () => {
|
|
61
|
+
return {
|
|
62
|
+
__esModule: true,
|
|
63
|
+
default: {
|
|
64
|
+
findOneBy: jest.fn(),
|
|
65
|
+
findBy: jest.fn(),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
jest.mock("../../../Server/Infrastructure/Semaphore", () => {
|
|
71
|
+
return {
|
|
72
|
+
__esModule: true,
|
|
73
|
+
default: {
|
|
74
|
+
lock: jest.fn(),
|
|
75
|
+
release: jest.fn(),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
jest.mock("../../../Server/Utils/Logger", () => {
|
|
81
|
+
return {
|
|
82
|
+
__esModule: true,
|
|
83
|
+
default: {
|
|
84
|
+
debug: jest.fn(),
|
|
85
|
+
info: jest.fn(),
|
|
86
|
+
warn: jest.fn(),
|
|
87
|
+
error: jest.fn(),
|
|
88
|
+
trace: jest.fn(),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
import NetworkDeviceAutoImportRuleEngineService, {
|
|
94
|
+
AUTO_IMPORT_SWEEP_LOCK_KEY,
|
|
95
|
+
AUTO_IMPORT_SWEEP_LOCK_NAMESPACE,
|
|
96
|
+
AUTO_IMPORT_SWEEP_LOCK_TIMEOUT_MS,
|
|
97
|
+
ExistingHostnamesByProjectId,
|
|
98
|
+
MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
99
|
+
MAX_RESULT_AGE_IN_HOURS,
|
|
100
|
+
MAX_SCANS_PER_AUTO_IMPORT_RULE_RUN,
|
|
101
|
+
} from "../../../Server/Services/NetworkDeviceAutoImportRuleEngineService";
|
|
102
|
+
import NetworkDeviceAutoImportRuleService from "../../../Server/Services/NetworkDeviceAutoImportRuleService";
|
|
103
|
+
import NetworkDeviceDiscoveryScanService from "../../../Server/Services/NetworkDeviceDiscoveryScanService";
|
|
104
|
+
import NetworkDeviceService from "../../../Server/Services/NetworkDeviceService";
|
|
105
|
+
import Semaphore from "../../../Server/Infrastructure/Semaphore";
|
|
106
|
+
import logger from "../../../Server/Utils/Logger";
|
|
107
|
+
import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
|
|
108
|
+
import NetworkDeviceAutoImportRule from "../../../Models/DatabaseModels/NetworkDeviceAutoImportRule";
|
|
109
|
+
import NetworkDeviceDiscoveryScan, {
|
|
110
|
+
DiscoveredNetworkDevice,
|
|
111
|
+
} from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
|
|
112
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
113
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
114
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
115
|
+
import {
|
|
116
|
+
AutoImportRuleRunResult,
|
|
117
|
+
MAX_MATCHED_IP_SAMPLE,
|
|
118
|
+
} from "../../../Types/NetworkAutomation/RuleRunResult";
|
|
119
|
+
import { describe, expect, it, beforeEach } from "@jest/globals";
|
|
120
|
+
|
|
121
|
+
/*
|
|
122
|
+
* The engine yields the event loop between scans via setImmediate — a Node
|
|
123
|
+
* global that jsdom (this repo's jest environment) does not provide, though
|
|
124
|
+
* every real runtime for Server code does. Polyfilled here so the yield
|
|
125
|
+
* stays a yield.
|
|
126
|
+
*/
|
|
127
|
+
if (typeof globalThis.setImmediate === "undefined") {
|
|
128
|
+
(globalThis as Record<string, unknown>)["setImmediate"] = ((
|
|
129
|
+
callback: () => void,
|
|
130
|
+
): ReturnType<typeof setTimeout> => {
|
|
131
|
+
return setTimeout(callback, 0);
|
|
132
|
+
}) as unknown as typeof globalThis.setImmediate;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
136
|
+
"22222222-2222-4222-8222-222222222222",
|
|
137
|
+
);
|
|
138
|
+
const PROBE_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
|
|
139
|
+
const SCAN_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
|
|
140
|
+
const RULE_ID: ObjectID = new ObjectID("77777777-7777-4777-8777-777777777777");
|
|
141
|
+
|
|
142
|
+
// What the mocked Semaphore.lock hands back and release must get back.
|
|
143
|
+
const FAKE_MUTEX: { id: string } = { id: "fake-sweep-mutex" };
|
|
144
|
+
|
|
145
|
+
const createMock: jest.Mock =
|
|
146
|
+
NetworkDeviceService.create as unknown as jest.Mock;
|
|
147
|
+
const deviceFindByMock: jest.Mock =
|
|
148
|
+
NetworkDeviceService.findBy as unknown as jest.Mock;
|
|
149
|
+
const deviceFindOneByMock: jest.Mock =
|
|
150
|
+
NetworkDeviceService.findOneBy as unknown as jest.Mock;
|
|
151
|
+
const scanFindOneByMock: jest.Mock =
|
|
152
|
+
NetworkDeviceDiscoveryScanService.findOneBy as unknown as jest.Mock;
|
|
153
|
+
const scanFindByMock: jest.Mock =
|
|
154
|
+
NetworkDeviceDiscoveryScanService.findBy as unknown as jest.Mock;
|
|
155
|
+
const scanUpdateMock: jest.Mock =
|
|
156
|
+
NetworkDeviceDiscoveryScanService.updateColumnsByIdWithoutHooks as unknown as jest.Mock;
|
|
157
|
+
const ruleFindOneByMock: jest.Mock =
|
|
158
|
+
NetworkDeviceAutoImportRuleService.findOneBy as unknown as jest.Mock;
|
|
159
|
+
const ruleFindByMock: jest.Mock =
|
|
160
|
+
NetworkDeviceAutoImportRuleService.findBy as unknown as jest.Mock;
|
|
161
|
+
const semaphoreLockMock: jest.Mock = Semaphore.lock as unknown as jest.Mock;
|
|
162
|
+
const semaphoreReleaseMock: jest.Mock =
|
|
163
|
+
Semaphore.release as unknown as jest.Mock;
|
|
164
|
+
const loggerErrorMock: jest.Mock = logger.error as unknown as jest.Mock;
|
|
165
|
+
|
|
166
|
+
const RECENT_COMPLETED_AT: Date = OneUptimeDate.getCurrentDate();
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* The scan as the engine selects it: identity, results, and the credential
|
|
170
|
+
* columns the builder copies onto every SNMP device it creates.
|
|
171
|
+
*/
|
|
172
|
+
function makeScan(
|
|
173
|
+
overrides: Record<string, unknown> = {},
|
|
174
|
+
): NetworkDeviceDiscoveryScan {
|
|
175
|
+
return {
|
|
176
|
+
id: SCAN_ID,
|
|
177
|
+
_id: SCAN_ID.toString(),
|
|
178
|
+
projectId: PROJECT_ID,
|
|
179
|
+
status: "Completed",
|
|
180
|
+
completedAt: RECENT_COMPLETED_AT,
|
|
181
|
+
autoImportProcessedAt: undefined,
|
|
182
|
+
discoveredDevices: [],
|
|
183
|
+
probeId: PROBE_ID,
|
|
184
|
+
snmpVersion: "V2c",
|
|
185
|
+
snmpCommunityString: "public",
|
|
186
|
+
snmpPort: 161,
|
|
187
|
+
...overrides,
|
|
188
|
+
} as unknown as NetworkDeviceDiscoveryScan;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function makeRule(
|
|
192
|
+
overrides: Record<string, unknown> = {},
|
|
193
|
+
): NetworkDeviceAutoImportRule {
|
|
194
|
+
return {
|
|
195
|
+
id: RULE_ID,
|
|
196
|
+
_id: RULE_ID.toString(),
|
|
197
|
+
projectId: PROJECT_ID,
|
|
198
|
+
name: "Import the management subnet",
|
|
199
|
+
isEnabled: true,
|
|
200
|
+
isExclusion: false,
|
|
201
|
+
ipMatchTarget: "10.0.0.0/24",
|
|
202
|
+
...overrides,
|
|
203
|
+
} as unknown as NetworkDeviceAutoImportRule;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function makeHost(
|
|
207
|
+
overrides: Partial<DiscoveredNetworkDevice> = {},
|
|
208
|
+
): DiscoveredNetworkDevice {
|
|
209
|
+
return {
|
|
210
|
+
ipAddress: "10.0.0.5",
|
|
211
|
+
sysName: "core-switch-01",
|
|
212
|
+
sysDescr: "Cisco IOS Software, C2960X",
|
|
213
|
+
...overrides,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Enough unique in-target addresses to overrun the attempt budget.
|
|
218
|
+
function makeHostsBeyondTheCap(): Array<DiscoveredNetworkDevice> {
|
|
219
|
+
const hosts: Array<DiscoveredNetworkDevice> = [];
|
|
220
|
+
|
|
221
|
+
for (let i: number = 0; i < MAX_DEVICES_PER_AUTO_IMPORT_RUN + 2; i++) {
|
|
222
|
+
hosts.push(
|
|
223
|
+
makeHost({
|
|
224
|
+
ipAddress: `10.0.${Math.floor(i / 256)}.${i % 256}`,
|
|
225
|
+
sysName: `switch-${i}`,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return hosts;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
* Run Now reads scans in two phases: an {_id}-only stub listing, then one
|
|
235
|
+
* full re-read per stub — so only one multi-megabyte result set is ever
|
|
236
|
+
* resident. This wires both phases: findBy answers with stubs, findOneBy
|
|
237
|
+
* hands the full rows out one per call (null entries model a scan that
|
|
238
|
+
* vanished between the phases).
|
|
239
|
+
*/
|
|
240
|
+
function mockRunNowScans(
|
|
241
|
+
scans: Array<NetworkDeviceDiscoveryScan | null>,
|
|
242
|
+
): void {
|
|
243
|
+
scanFindByMock.mockResolvedValue(
|
|
244
|
+
scans.map((_scan: NetworkDeviceDiscoveryScan | null, index: number) => {
|
|
245
|
+
const suffix: string = index.toString(16).padStart(12, "0");
|
|
246
|
+
return {
|
|
247
|
+
id: new ObjectID(`00000000-0000-4000-8000-${suffix}`),
|
|
248
|
+
} as unknown as NetworkDeviceDiscoveryScan;
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
let nextScanIndex: number = 0;
|
|
253
|
+
scanFindOneByMock.mockImplementation(() => {
|
|
254
|
+
return Promise.resolve(scans[nextScanIndex++] ?? null);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function processScan(): Promise<AutoImportRuleRunResult | null> {
|
|
259
|
+
const cache: ExistingHostnamesByProjectId = new Map();
|
|
260
|
+
return NetworkDeviceAutoImportRuleEngineService.processCompletedScan({
|
|
261
|
+
scanId: SCAN_ID,
|
|
262
|
+
existingHostnamesByProjectId: cache,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function runRule(isDryRun: boolean): Promise<AutoImportRuleRunResult> {
|
|
267
|
+
return NetworkDeviceAutoImportRuleEngineService.applyRuleToCompletedScans({
|
|
268
|
+
ruleId: RULE_ID,
|
|
269
|
+
projectId: PROJECT_ID,
|
|
270
|
+
isDryRun: isDryRun,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// The device the engine handed to NetworkDeviceService.create, per call.
|
|
275
|
+
function createdDevice(callIndex: number): NetworkDevice {
|
|
276
|
+
return createMock.mock.calls[callIndex]![0].data as NetworkDevice;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
beforeEach(() => {
|
|
280
|
+
jest.clearAllMocks();
|
|
281
|
+
|
|
282
|
+
/*
|
|
283
|
+
* An empty inventory, a working create, a working stamp, an acquirable
|
|
284
|
+
* lock — the happy defaults each test narrows as needed.
|
|
285
|
+
*/
|
|
286
|
+
deviceFindByMock.mockResolvedValue([]);
|
|
287
|
+
deviceFindOneByMock.mockResolvedValue(null);
|
|
288
|
+
createMock.mockResolvedValue({});
|
|
289
|
+
scanUpdateMock.mockResolvedValue(undefined);
|
|
290
|
+
ruleFindByMock.mockResolvedValue([makeRule()]);
|
|
291
|
+
semaphoreLockMock.mockResolvedValue(FAKE_MUTEX);
|
|
292
|
+
semaphoreReleaseMock.mockResolvedValue(undefined);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("NetworkDeviceAutoImportRuleEngineService.processCompletedScan", () => {
|
|
296
|
+
/*
|
|
297
|
+
* The marker is the "these exact results were already consumed" signal;
|
|
298
|
+
* re-processing a stamped scan every tick would re-evaluate (and re-log)
|
|
299
|
+
* the same results forever.
|
|
300
|
+
*/
|
|
301
|
+
it("does nothing for a scan whose marker is already stamped", async () => {
|
|
302
|
+
scanFindOneByMock.mockResolvedValue(
|
|
303
|
+
makeScan({ autoImportProcessedAt: OneUptimeDate.getCurrentDate() }),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
307
|
+
|
|
308
|
+
expect(result).toBeNull();
|
|
309
|
+
expect(ruleFindByMock).not.toHaveBeenCalled();
|
|
310
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
311
|
+
expect(scanUpdateMock).not.toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("does nothing when the scan is gone", async () => {
|
|
315
|
+
scanFindOneByMock.mockResolvedValue(null);
|
|
316
|
+
|
|
317
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
318
|
+
|
|
319
|
+
expect(result).toBeNull();
|
|
320
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
321
|
+
expect(scanUpdateMock).not.toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
/*
|
|
325
|
+
* With no import rules nothing can ever import, so the results are
|
|
326
|
+
* retired (stamped) rather than left to accumulate as an ever-growing
|
|
327
|
+
* unprocessed backlog — and the stamp is a compare-and-set on the exact
|
|
328
|
+
* (status, completedAt) this pass read, so it can never retire results it
|
|
329
|
+
* did not see.
|
|
330
|
+
*/
|
|
331
|
+
it("stamps the marker and creates nothing when the project has no import rules", async () => {
|
|
332
|
+
scanFindOneByMock.mockResolvedValue(
|
|
333
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
334
|
+
);
|
|
335
|
+
// Only an exclusion rule: vetoes exist, nothing can import.
|
|
336
|
+
ruleFindByMock.mockResolvedValue([makeRule({ isExclusion: true })]);
|
|
337
|
+
|
|
338
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
339
|
+
|
|
340
|
+
expect(result).toBeNull();
|
|
341
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
342
|
+
|
|
343
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(1);
|
|
344
|
+
const updateCall: any = scanUpdateMock.mock.calls[0]![0];
|
|
345
|
+
expect(updateCall.id.toString()).toBe(SCAN_ID.toString());
|
|
346
|
+
expect(updateCall.data.autoImportProcessedAt).toBeInstanceOf(Date);
|
|
347
|
+
// Nothing was created, so the stored results are not rewritten.
|
|
348
|
+
expect(Object.keys(updateCall.data)).toEqual(["autoImportProcessedAt"]);
|
|
349
|
+
expect(updateCall.expectedData).toEqual({
|
|
350
|
+
status: "Completed",
|
|
351
|
+
completedAt: RECENT_COMPLETED_AT,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("imports the matching host and flips only its row in the write-back", async () => {
|
|
356
|
+
/*
|
|
357
|
+
* Kept as references: the write-back contract is about identity, not
|
|
358
|
+
* just shape — untouched rows (junk included) must pass through
|
|
359
|
+
* verbatim so the stored jsonb stays exactly what the probe sent.
|
|
360
|
+
*/
|
|
361
|
+
const junkRow: DiscoveredNetworkDevice =
|
|
362
|
+
null as unknown as DiscoveredNetworkDevice;
|
|
363
|
+
const matchingRow: DiscoveredNetworkDevice = makeHost();
|
|
364
|
+
const unmatchedRow: DiscoveredNetworkDevice = makeHost({
|
|
365
|
+
ipAddress: "192.168.1.5",
|
|
366
|
+
sysName: "printer-01",
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
scanFindOneByMock.mockResolvedValue(
|
|
370
|
+
makeScan({ discoveredDevices: [junkRow, matchingRow, unmatchedRow] }),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
374
|
+
|
|
375
|
+
// The junk row is dropped from evaluation, not from storage.
|
|
376
|
+
expect(result).toMatchObject({
|
|
377
|
+
hostsEvaluated: 2,
|
|
378
|
+
hostsMatched: 1,
|
|
379
|
+
hostsExcluded: 0,
|
|
380
|
+
hostsSkippedAlreadyRegistered: 0,
|
|
381
|
+
devicesCreated: 1,
|
|
382
|
+
devicesFailed: 0,
|
|
383
|
+
isTruncated: false,
|
|
384
|
+
isDryRun: false,
|
|
385
|
+
matchedIpAddressSample: ["10.0.0.5"],
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
389
|
+
const device: NetworkDevice = createdDevice(0);
|
|
390
|
+
// The address is the hostname AND the dedup key downstream.
|
|
391
|
+
expect(device.hostname).toBe("10.0.0.5");
|
|
392
|
+
expect(device.name).toBe("core-switch-01");
|
|
393
|
+
expect(device.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
394
|
+
/*
|
|
395
|
+
* Zero-touch pipeline: nobody reviews an auto-imported device, so the
|
|
396
|
+
* vendor-template banner would never be clicked — the engine opts the
|
|
397
|
+
* device into the poll-time auto-apply instead.
|
|
398
|
+
*/
|
|
399
|
+
expect(device.autoApplyVendorHealthTemplate).toBe(true);
|
|
400
|
+
// Built from THIS scan: the scan's credentials rode along.
|
|
401
|
+
expect(device.snmpVersion).toBe("V2c");
|
|
402
|
+
expect(createMock.mock.calls[0]![0].props).toEqual({ isRoot: true });
|
|
403
|
+
|
|
404
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(1);
|
|
405
|
+
const updateCall: any = scanUpdateMock.mock.calls[0]![0];
|
|
406
|
+
expect(updateCall.data.autoImportProcessedAt).toBeInstanceOf(Date);
|
|
407
|
+
|
|
408
|
+
const restamped: Array<unknown> = updateCall.data.discoveredDevices;
|
|
409
|
+
expect(restamped).toHaveLength(3);
|
|
410
|
+
expect(restamped[0]).toBeNull();
|
|
411
|
+
// The created row flips its flag so the Review dialog stops offering it.
|
|
412
|
+
expect(restamped[1]).toEqual({
|
|
413
|
+
...matchingRow,
|
|
414
|
+
isAlreadyRegistered: true,
|
|
415
|
+
});
|
|
416
|
+
// The unmatched row is the SAME object, not a rewritten copy.
|
|
417
|
+
expect(restamped[2]).toBe(unmatchedRow);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
/*
|
|
421
|
+
* Recurring scans re-report the same hosts every interval; seeing the
|
|
422
|
+
* same results twice must create nothing twice.
|
|
423
|
+
*/
|
|
424
|
+
it("skips a host whose address is already in the inventory", async () => {
|
|
425
|
+
scanFindOneByMock.mockResolvedValue(
|
|
426
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
427
|
+
);
|
|
428
|
+
deviceFindByMock.mockResolvedValue([
|
|
429
|
+
{ hostname: "10.0.0.5" } as unknown as NetworkDevice,
|
|
430
|
+
]);
|
|
431
|
+
|
|
432
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
433
|
+
|
|
434
|
+
expect(result).toMatchObject({
|
|
435
|
+
hostsEvaluated: 1,
|
|
436
|
+
hostsMatched: 1,
|
|
437
|
+
hostsSkippedAlreadyRegistered: 1,
|
|
438
|
+
devicesCreated: 0,
|
|
439
|
+
});
|
|
440
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
441
|
+
|
|
442
|
+
// Nothing created, so the stamp carries the marker alone.
|
|
443
|
+
const updateCall: any = scanUpdateMock.mock.calls[0]![0];
|
|
444
|
+
expect(Object.keys(updateCall.data)).toEqual(["autoImportProcessedAt"]);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
/*
|
|
448
|
+
* An over-long "address" is junk that would fail the varchar(100)
|
|
449
|
+
* hostname on every pass, identically, forever — so it is refused before
|
|
450
|
+
* any counting rather than logged as a devicesFailed per worker tick.
|
|
451
|
+
*/
|
|
452
|
+
it("skips a host whose address could never be a hostname, before any counting", async () => {
|
|
453
|
+
scanFindOneByMock.mockResolvedValue(
|
|
454
|
+
makeScan({
|
|
455
|
+
discoveredDevices: [
|
|
456
|
+
makeHost({ ipAddress: "9".repeat(101) }),
|
|
457
|
+
makeHost(),
|
|
458
|
+
],
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
463
|
+
|
|
464
|
+
// The junk row was never evaluated, let alone matched or attempted.
|
|
465
|
+
expect(result).toMatchObject({
|
|
466
|
+
hostsEvaluated: 1,
|
|
467
|
+
hostsMatched: 1,
|
|
468
|
+
devicesCreated: 1,
|
|
469
|
+
devicesFailed: 0,
|
|
470
|
+
});
|
|
471
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
472
|
+
expect(createdDevice(0).hostname).toBe("10.0.0.5");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("the create-failure protocol", () => {
|
|
476
|
+
/*
|
|
477
|
+
* Names are unique per project while addresses are not, and two devices
|
|
478
|
+
* legitimately sharing a sysName is common on real estates. When the
|
|
479
|
+
* ADDRESS is still free after a failed create, the name was the problem
|
|
480
|
+
* — one retry under the address-suffixed fallback settles it.
|
|
481
|
+
*/
|
|
482
|
+
it("retries a name collision once under the fallback name", async () => {
|
|
483
|
+
scanFindOneByMock.mockResolvedValue(
|
|
484
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
485
|
+
);
|
|
486
|
+
createMock
|
|
487
|
+
.mockRejectedValueOnce(
|
|
488
|
+
new BadDataException("Network Device with this name already exists."),
|
|
489
|
+
)
|
|
490
|
+
.mockResolvedValueOnce({});
|
|
491
|
+
// The address re-check finds nothing: the name really was the problem.
|
|
492
|
+
deviceFindOneByMock.mockResolvedValue(null);
|
|
493
|
+
|
|
494
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
495
|
+
|
|
496
|
+
expect(createMock).toHaveBeenCalledTimes(2);
|
|
497
|
+
// Same device both times; only the name changed.
|
|
498
|
+
expect(createdDevice(0).name).toBe("core-switch-01");
|
|
499
|
+
expect(createdDevice(1).name).toBe("core-switch-01 (10.0.0.5)");
|
|
500
|
+
expect(createdDevice(1).name!.endsWith(" (10.0.0.5)")).toBe(true);
|
|
501
|
+
expect(createdDevice(1).hostname).toBe("10.0.0.5");
|
|
502
|
+
|
|
503
|
+
expect(result).toMatchObject({
|
|
504
|
+
devicesCreated: 1,
|
|
505
|
+
devicesFailed: 0,
|
|
506
|
+
hostsSkippedAlreadyRegistered: 0,
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
/*
|
|
511
|
+
* Losing a race to a concurrent import of the SAME host must read as
|
|
512
|
+
* "already registered" — retrying under a new name here would spawn a
|
|
513
|
+
* renamed twin of a device that now exists.
|
|
514
|
+
*/
|
|
515
|
+
it("reads a lost create race as already-registered, with no rename retry", async () => {
|
|
516
|
+
scanFindOneByMock.mockResolvedValue(
|
|
517
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
518
|
+
);
|
|
519
|
+
createMock.mockRejectedValueOnce(
|
|
520
|
+
new BadDataException("Network Device with this name already exists."),
|
|
521
|
+
);
|
|
522
|
+
// The address re-check finds a device: someone imported it meanwhile.
|
|
523
|
+
deviceFindOneByMock.mockResolvedValue({
|
|
524
|
+
id: new ObjectID("44444444-4444-4444-8444-444444444444"),
|
|
525
|
+
} as unknown as NetworkDevice);
|
|
526
|
+
|
|
527
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
528
|
+
|
|
529
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
530
|
+
expect(result).toMatchObject({
|
|
531
|
+
hostsMatched: 1,
|
|
532
|
+
hostsSkippedAlreadyRegistered: 1,
|
|
533
|
+
devicesCreated: 0,
|
|
534
|
+
devicesFailed: 0,
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
/*
|
|
540
|
+
* The cap turns "a typo'd rule meets a huge subnet" into a few paced
|
|
541
|
+
* worker ticks instead of one burst of thousands of creates — and the
|
|
542
|
+
* UNSTAMPED marker is what makes the next tick resume where this one
|
|
543
|
+
* stopped, with idempotency skipping everything already created.
|
|
544
|
+
*/
|
|
545
|
+
it("stops at the device cap and leaves the marker unstamped so the next tick resumes", async () => {
|
|
546
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeHostsBeyondTheCap();
|
|
547
|
+
|
|
548
|
+
scanFindOneByMock.mockResolvedValue(makeScan({ discoveredDevices: hosts }));
|
|
549
|
+
ruleFindByMock.mockResolvedValue([
|
|
550
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
551
|
+
]);
|
|
552
|
+
|
|
553
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
554
|
+
|
|
555
|
+
expect(createMock).toHaveBeenCalledTimes(MAX_DEVICES_PER_AUTO_IMPORT_RUN);
|
|
556
|
+
expect(result).toMatchObject({
|
|
557
|
+
devicesCreated: MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
558
|
+
isTruncated: true,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// The write-back still retires what WAS created…
|
|
562
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(1);
|
|
563
|
+
const updateCall: any = scanUpdateMock.mock.calls[0]![0];
|
|
564
|
+
expect(updateCall.data.discoveredDevices).toHaveLength(hosts.length);
|
|
565
|
+
// …but the marker stays NULL: this scan is not done.
|
|
566
|
+
expect(Object.keys(updateCall.data)).not.toContain("autoImportProcessedAt");
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
/*
|
|
570
|
+
* The resume protocol's other half: a truncated pass that created NOTHING
|
|
571
|
+
* — every attempt failed — is stamped anyway. Leaving the marker NULL
|
|
572
|
+
* would have the sweep repeat the identical doomed pass every tick
|
|
573
|
+
* forever; the operator gets an error log and Run Now instead.
|
|
574
|
+
*/
|
|
575
|
+
it("stamps a zero-progress truncated pass instead of retrying it forever", async () => {
|
|
576
|
+
scanFindOneByMock.mockResolvedValue(
|
|
577
|
+
makeScan({ discoveredDevices: makeHostsBeyondTheCap() }),
|
|
578
|
+
);
|
|
579
|
+
ruleFindByMock.mockResolvedValue([
|
|
580
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
581
|
+
]);
|
|
582
|
+
// Every create fails, first attempt and fallback-name retry alike…
|
|
583
|
+
createMock.mockRejectedValue(new BadDataException("create is broken"));
|
|
584
|
+
// …and the address re-check keeps saying the host is NOT registered.
|
|
585
|
+
deviceFindOneByMock.mockResolvedValue(null);
|
|
586
|
+
|
|
587
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
588
|
+
|
|
589
|
+
expect(result).toMatchObject({
|
|
590
|
+
devicesCreated: 0,
|
|
591
|
+
devicesFailed: MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
592
|
+
isTruncated: true,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// The marker IS stamped: this pass is retired, not resumed.
|
|
596
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(1);
|
|
597
|
+
const updateCall: any = scanUpdateMock.mock.calls[0]![0];
|
|
598
|
+
expect(Object.keys(updateCall.data)).toEqual(["autoImportProcessedAt"]);
|
|
599
|
+
|
|
600
|
+
// And the operator is told, loudly, why nothing imported.
|
|
601
|
+
expect(
|
|
602
|
+
loggerErrorMock.mock.calls.some((call: Array<unknown>) => {
|
|
603
|
+
return String(call[0]).includes("every create failing");
|
|
604
|
+
}),
|
|
605
|
+
).toBe(true);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
/*
|
|
609
|
+
* A late result can land on a reaper-Failed scan hours after its sweep,
|
|
610
|
+
* and a worker outage leaves a backlog; silently importing an hours-old
|
|
611
|
+
* host list is the wrong surprise. Old results are retired unimported —
|
|
612
|
+
* Run Now remains the deliberate way to reach them.
|
|
613
|
+
*/
|
|
614
|
+
it("stamps results older than MAX_RESULT_AGE_IN_HOURS without importing", async () => {
|
|
615
|
+
scanFindOneByMock.mockResolvedValue(
|
|
616
|
+
makeScan({
|
|
617
|
+
discoveredDevices: [makeHost()],
|
|
618
|
+
completedAt: OneUptimeDate.getSomeHoursAgo(MAX_RESULT_AGE_IN_HOURS + 1),
|
|
619
|
+
}),
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
623
|
+
|
|
624
|
+
expect(result).toBeNull();
|
|
625
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
626
|
+
|
|
627
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(1);
|
|
628
|
+
const updateCall: any = scanUpdateMock.mock.calls[0]![0];
|
|
629
|
+
expect(Object.keys(updateCall.data)).toEqual(["autoImportProcessedAt"]);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
describe("NetworkDeviceAutoImportRuleEngineService.applyRuleToCompletedScans", () => {
|
|
634
|
+
it("evaluates everything and writes nothing on a dry run", async () => {
|
|
635
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
636
|
+
/*
|
|
637
|
+
* The project's enabled rules: an exclusion that must still veto, and
|
|
638
|
+
* an UNRELATED import rule that must NOT ride along — Run Now applies
|
|
639
|
+
* ONE rule, not the whole rule set.
|
|
640
|
+
*/
|
|
641
|
+
ruleFindByMock.mockResolvedValue([
|
|
642
|
+
makeRule({
|
|
643
|
+
id: new ObjectID("55555555-5555-4555-8555-555555555555"),
|
|
644
|
+
isExclusion: true,
|
|
645
|
+
ipMatchTarget: "10.0.0.9",
|
|
646
|
+
}),
|
|
647
|
+
makeRule({
|
|
648
|
+
id: new ObjectID("66666666-6666-4666-8666-666666666666"),
|
|
649
|
+
ipMatchTarget: "192.168.0.0/16",
|
|
650
|
+
}),
|
|
651
|
+
]);
|
|
652
|
+
mockRunNowScans([
|
|
653
|
+
makeScan({
|
|
654
|
+
discoveredDevices: [
|
|
655
|
+
makeHost(),
|
|
656
|
+
makeHost({ ipAddress: "10.0.0.9", sysName: "phone-01" }),
|
|
657
|
+
makeHost({ ipAddress: "192.168.1.5", sysName: "printer-01" }),
|
|
658
|
+
],
|
|
659
|
+
}),
|
|
660
|
+
]);
|
|
661
|
+
|
|
662
|
+
const result: AutoImportRuleRunResult = await runRule(true);
|
|
663
|
+
|
|
664
|
+
expect(result).toMatchObject({
|
|
665
|
+
hostsEvaluated: 3,
|
|
666
|
+
// Only the run rule's subnet — the other import rule did not apply.
|
|
667
|
+
hostsMatched: 1,
|
|
668
|
+
// "Run Now" is not a way around a veto.
|
|
669
|
+
hostsExcluded: 1,
|
|
670
|
+
devicesCreated: 0,
|
|
671
|
+
isDryRun: true,
|
|
672
|
+
isTruncated: false,
|
|
673
|
+
hasMoreScans: false,
|
|
674
|
+
});
|
|
675
|
+
// The operator's "which hosts would this claim" answer.
|
|
676
|
+
expect(result.matchedIpAddressSample).toEqual(["10.0.0.5"]);
|
|
677
|
+
|
|
678
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
679
|
+
// No creates, so no write-back either: a dry run touches no row.
|
|
680
|
+
expect(scanUpdateMock).not.toHaveBeenCalled();
|
|
681
|
+
// And no lock: a run that writes nothing has nothing to serialize.
|
|
682
|
+
expect(semaphoreLockMock).not.toHaveBeenCalled();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
/*
|
|
686
|
+
* The two-phase scan read: the listing is {_id} stubs only, and each scan
|
|
687
|
+
* is re-read individually with its status re-checked — so one manual run
|
|
688
|
+
* never holds every scan's multi-megabyte jsonb at once, and never
|
|
689
|
+
* evaluates a scan that was re-queued after the listing.
|
|
690
|
+
*/
|
|
691
|
+
it("lists scan stubs first and re-reads each scan with a status re-check", async () => {
|
|
692
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
693
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
694
|
+
mockRunNowScans([makeScan({ discoveredDevices: [makeHost()] })]);
|
|
695
|
+
|
|
696
|
+
await runRule(true);
|
|
697
|
+
|
|
698
|
+
expect(scanFindByMock).toHaveBeenCalledTimes(1);
|
|
699
|
+
const listCall: any = scanFindByMock.mock.calls[0]![0];
|
|
700
|
+
expect(listCall.select).toEqual({ _id: true });
|
|
701
|
+
// One extra row settles "were there more scans" honestly.
|
|
702
|
+
expect(listCall.limit).toBe(MAX_SCANS_PER_AUTO_IMPORT_RULE_RUN + 1);
|
|
703
|
+
|
|
704
|
+
expect(scanFindOneByMock).toHaveBeenCalledTimes(1);
|
|
705
|
+
const rereadCall: any = scanFindOneByMock.mock.calls[0]![0];
|
|
706
|
+
expect(rereadCall.query.status).toBe("Completed");
|
|
707
|
+
expect(rereadCall.query.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
708
|
+
expect(rereadCall.select.discoveredDevices).toBe(true);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("skips a scan that vanished between the stub listing and the re-read", async () => {
|
|
712
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
713
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
714
|
+
// First stub's re-read finds nothing; the second still evaluates.
|
|
715
|
+
mockRunNowScans([null, makeScan({ discoveredDevices: [makeHost()] })]);
|
|
716
|
+
|
|
717
|
+
const result: AutoImportRuleRunResult = await runRule(true);
|
|
718
|
+
|
|
719
|
+
expect(scanFindOneByMock).toHaveBeenCalledTimes(2);
|
|
720
|
+
expect(result).toMatchObject({
|
|
721
|
+
hostsEvaluated: 1,
|
|
722
|
+
hostsMatched: 1,
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
/*
|
|
727
|
+
* Scan-count truncation is its own flag, not isTruncated: the advice
|
|
728
|
+
* differs (re-running re-reads the same newest scans; only the device cap
|
|
729
|
+
* is resumable by running again).
|
|
730
|
+
*/
|
|
731
|
+
it("reports hasMoreScans, not isTruncated, when the project has more scans than one run reads", async () => {
|
|
732
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
733
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
734
|
+
mockRunNowScans(
|
|
735
|
+
Array.from(
|
|
736
|
+
{ length: MAX_SCANS_PER_AUTO_IMPORT_RULE_RUN + 1 },
|
|
737
|
+
(): NetworkDeviceDiscoveryScan => {
|
|
738
|
+
return makeScan({ discoveredDevices: [] });
|
|
739
|
+
},
|
|
740
|
+
),
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
const result: AutoImportRuleRunResult = await runRule(true);
|
|
744
|
+
|
|
745
|
+
expect(result.hasMoreScans).toBe(true);
|
|
746
|
+
expect(result.isTruncated).toBe(false);
|
|
747
|
+
// The extra row was only a probe: it is not read as a scan.
|
|
748
|
+
expect(scanFindOneByMock).toHaveBeenCalledTimes(
|
|
749
|
+
MAX_SCANS_PER_AUTO_IMPORT_RULE_RUN,
|
|
750
|
+
);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
/*
|
|
754
|
+
* A dry run must promise what a real run would DO — and a host listed on
|
|
755
|
+
* duplicate rows (or in two overlapping scans) imports once, so the dry
|
|
756
|
+
* run counts it once and reads its later appearances as already
|
|
757
|
+
* registered.
|
|
758
|
+
*/
|
|
759
|
+
it("counts a host listed twice as one import and one skip on a dry run", async () => {
|
|
760
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
761
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
762
|
+
mockRunNowScans([
|
|
763
|
+
makeScan({ discoveredDevices: [makeHost(), makeHost()] }),
|
|
764
|
+
]);
|
|
765
|
+
|
|
766
|
+
const result: AutoImportRuleRunResult = await runRule(true);
|
|
767
|
+
|
|
768
|
+
expect(result).toMatchObject({
|
|
769
|
+
hostsEvaluated: 2,
|
|
770
|
+
hostsMatched: 2,
|
|
771
|
+
hostsSkippedAlreadyRegistered: 1,
|
|
772
|
+
devicesCreated: 0,
|
|
773
|
+
isDryRun: true,
|
|
774
|
+
});
|
|
775
|
+
expect(result.matchedIpAddressSample).toEqual(["10.0.0.5"]);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
/*
|
|
779
|
+
* The attempt budget bounds dry runs too: a dry run over a /16 walks the
|
|
780
|
+
* same capped amount of work inside its API request as the real run it
|
|
781
|
+
* predicts, and says so through the same flag.
|
|
782
|
+
*/
|
|
783
|
+
it("bounds a dry run by the device cap and reports the truncation", async () => {
|
|
784
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
785
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
786
|
+
);
|
|
787
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
788
|
+
mockRunNowScans([makeScan({ discoveredDevices: makeHostsBeyondTheCap() })]);
|
|
789
|
+
|
|
790
|
+
const result: AutoImportRuleRunResult = await runRule(true);
|
|
791
|
+
|
|
792
|
+
expect(result).toMatchObject({
|
|
793
|
+
isTruncated: true,
|
|
794
|
+
devicesCreated: 0,
|
|
795
|
+
isDryRun: true,
|
|
796
|
+
});
|
|
797
|
+
expect(result.matchedIpAddressSample).toHaveLength(MAX_MATCHED_IP_SAMPLE);
|
|
798
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
799
|
+
expect(scanUpdateMock).not.toHaveBeenCalled();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
describe("a real run and the sweep lock", () => {
|
|
803
|
+
/*
|
|
804
|
+
* The engine's idempotency is check-then-create with no DB backstop, so
|
|
805
|
+
* a real Run Now must hold the SAME Redis lock as the worker sweep —
|
|
806
|
+
* manual-vs-sweep and manual-vs-manual can never interleave.
|
|
807
|
+
*/
|
|
808
|
+
it("creates devices under the sweep lock and releases it after", async () => {
|
|
809
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
810
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
811
|
+
mockRunNowScans([makeScan({ discoveredDevices: [makeHost()] })]);
|
|
812
|
+
|
|
813
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
814
|
+
|
|
815
|
+
expect(result).toMatchObject({
|
|
816
|
+
hostsMatched: 1,
|
|
817
|
+
devicesCreated: 1,
|
|
818
|
+
isDryRun: false,
|
|
819
|
+
hasMoreScans: false,
|
|
820
|
+
});
|
|
821
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
822
|
+
|
|
823
|
+
// The worker's lock, by the shared constants — never a lookalike.
|
|
824
|
+
expect(semaphoreLockMock).toHaveBeenCalledTimes(1);
|
|
825
|
+
expect(semaphoreLockMock.mock.calls[0]![0]).toMatchObject({
|
|
826
|
+
key: AUTO_IMPORT_SWEEP_LOCK_KEY,
|
|
827
|
+
namespace: AUTO_IMPORT_SWEEP_LOCK_NAMESPACE,
|
|
828
|
+
lockTimeout: AUTO_IMPORT_SWEEP_LOCK_TIMEOUT_MS,
|
|
829
|
+
});
|
|
830
|
+
expect(semaphoreReleaseMock).toHaveBeenCalledTimes(1);
|
|
831
|
+
expect(semaphoreReleaseMock).toHaveBeenCalledWith(FAKE_MUTEX);
|
|
832
|
+
|
|
833
|
+
/*
|
|
834
|
+
* The write-back retires the imported host for the Review dialog, but
|
|
835
|
+
* the MARKER is deliberately not stamped: a manual run of one rule
|
|
836
|
+
* has not done what the full rule set would, so the automatic path
|
|
837
|
+
* must still get its turn at these results.
|
|
838
|
+
*/
|
|
839
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(1);
|
|
840
|
+
const updateCall: any = scanUpdateMock.mock.calls[0]![0];
|
|
841
|
+
expect(Object.keys(updateCall.data)).toEqual(["discoveredDevices"]);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
/*
|
|
845
|
+
* A held lock means the sweep is importing right now; failing fast with
|
|
846
|
+
* an explanation beats blocking an API request behind a sweep that may
|
|
847
|
+
* run for minutes.
|
|
848
|
+
*/
|
|
849
|
+
it("refuses a real run when the sweep lock cannot be acquired", async () => {
|
|
850
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
851
|
+
semaphoreLockMock.mockRejectedValue(new Error("lock is held"));
|
|
852
|
+
|
|
853
|
+
await expect(runRule(false)).rejects.toThrow(BadDataException);
|
|
854
|
+
await expect(runRule(false)).rejects.toThrow(
|
|
855
|
+
/automatic import sweep is currently running/,
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
expect(scanFindByMock).not.toHaveBeenCalled();
|
|
859
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
860
|
+
// No lock was acquired, so there is nothing to release.
|
|
861
|
+
expect(semaphoreReleaseMock).not.toHaveBeenCalled();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// A leaked sweep lock blocks every future run AND the worker sweep.
|
|
865
|
+
it("releases the lock even when the run body throws", async () => {
|
|
866
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
867
|
+
// The first thing the run body does is load the exclusion rules.
|
|
868
|
+
ruleFindByMock.mockRejectedValue(new Error("database is down"));
|
|
869
|
+
|
|
870
|
+
await expect(runRule(false)).rejects.toThrow("database is down");
|
|
871
|
+
|
|
872
|
+
expect(semaphoreLockMock).toHaveBeenCalledTimes(1);
|
|
873
|
+
expect(semaphoreReleaseMock).toHaveBeenCalledTimes(1);
|
|
874
|
+
expect(semaphoreReleaseMock).toHaveBeenCalledWith(FAKE_MUTEX);
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
describe("rule resolution", () => {
|
|
879
|
+
/*
|
|
880
|
+
* The point of a dry run: answer "what would this rule import" BEFORE
|
|
881
|
+
* enabling it against live scans. Only a real run of a disabled rule
|
|
882
|
+
* contradicts the toggle next to the button.
|
|
883
|
+
*/
|
|
884
|
+
it("dry-runs a disabled rule", async () => {
|
|
885
|
+
ruleFindOneByMock.mockResolvedValue(makeRule({ isEnabled: false }));
|
|
886
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
887
|
+
mockRunNowScans([makeScan({ discoveredDevices: [makeHost()] })]);
|
|
888
|
+
|
|
889
|
+
const result: AutoImportRuleRunResult = await runRule(true);
|
|
890
|
+
|
|
891
|
+
expect(result).toMatchObject({
|
|
892
|
+
hostsEvaluated: 1,
|
|
893
|
+
hostsMatched: 1,
|
|
894
|
+
devicesCreated: 0,
|
|
895
|
+
isDryRun: true,
|
|
896
|
+
});
|
|
897
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
898
|
+
expect(scanUpdateMock).not.toHaveBeenCalled();
|
|
899
|
+
expect(semaphoreLockMock).not.toHaveBeenCalled();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it("refuses a REAL run of a disabled rule, pointing at Dry Run", async () => {
|
|
903
|
+
ruleFindOneByMock.mockResolvedValue(makeRule({ isEnabled: false }));
|
|
904
|
+
|
|
905
|
+
await expect(runRule(false)).rejects.toThrow(
|
|
906
|
+
"This auto-import rule is disabled. Enable it before running it, or use Dry Run to preview what it would import.",
|
|
907
|
+
);
|
|
908
|
+
expect(scanFindByMock).not.toHaveBeenCalled();
|
|
909
|
+
expect(semaphoreLockMock).not.toHaveBeenCalled();
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
/*
|
|
913
|
+
* An exclusion rule imports nothing by itself, so "running" it can only
|
|
914
|
+
* mislead — the refusal explains what to run instead.
|
|
915
|
+
*/
|
|
916
|
+
it("refuses to run an exclusion rule", async () => {
|
|
917
|
+
ruleFindOneByMock.mockResolvedValue(makeRule({ isExclusion: true }));
|
|
918
|
+
|
|
919
|
+
await expect(runRule(false)).rejects.toThrow(BadDataException);
|
|
920
|
+
expect(scanFindByMock).not.toHaveBeenCalled();
|
|
921
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
922
|
+
expect(semaphoreLockMock).not.toHaveBeenCalled();
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it("rejects a rule id that does not resolve in the project", async () => {
|
|
926
|
+
ruleFindOneByMock.mockResolvedValue(null);
|
|
927
|
+
|
|
928
|
+
await expect(runRule(false)).rejects.toThrow(
|
|
929
|
+
"Auto-import rule not found.",
|
|
930
|
+
);
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
});
|