@oneuptime/common 12.0.23 → 12.0.25
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/Span.ts +101 -0
- package/Models/DatabaseModels/DetectionRule.ts +55 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
- package/Models/DatabaseModels/Project.ts +68 -0
- package/Models/DatabaseModels/User.ts +68 -0
- package/Server/API/BaseAPI.ts +0 -24
- package/Server/API/SlackAPI.ts +0 -2
- package/Server/API/TelemetryAPI.ts +29 -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 +6 -0
- package/Server/Middleware/GzipRequestBody.ts +311 -0
- package/Server/Middleware/MultipartFormData.ts +87 -3
- package/Server/Middleware/SlackAuthorization.ts +96 -18
- package/Server/Services/DetectionRuleService.ts +73 -6
- package/Server/Services/Index.ts +205 -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/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 +181 -0
- package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
- package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
- 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/LlmCallsTableIdentity.test.tsx +322 -0
- package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
- package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
- package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -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/BaseAPI.test.ts +41 -0
- package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
- 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/Middleware/SlackAuthorization.test.ts +262 -5
- package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -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/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/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
- package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
- package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +387 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +386 -0
- package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -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 +584 -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/DiscoveredHostUtil.test.ts +221 -0
- package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
- package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
- package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +770 -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/SecurityEvent/DetectionFindingConstants.ts +11 -0
- package/Types/Telemetry/LlmConventions.ts +255 -0
- package/Types/Telemetry/LlmMetricConventions.ts +329 -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/ScanNameUtil.ts +181 -0
- package/Utils/StatusPage/ResourceExplorer.ts +258 -0
- package/Utils/Telemetry/LlmMetricQuery.ts +287 -0
- package/build/dist/Models/AnalyticsModels/Span.js +89 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -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/BaseAPI.js +3 -19
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +0 -2
- package/build/dist/Server/API/SlackAPI.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/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 +6 -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/Middleware/SlackAuthorization.js +58 -7
- package/build/dist/Server/Middleware/SlackAuthorization.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 +204 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- 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/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 +151 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
- 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/SecurityEvent/DetectionFindingConstants.js +9 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmConventions.js +236 -0
- package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +290 -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/ScanNameUtil.js +120 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
- 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 +175 -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,564 @@
|
|
|
1
|
+
import TelemetryAttributeService from "../../../Server/Services/TelemetryAttributeService";
|
|
2
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
3
|
+
import {
|
|
4
|
+
ExpressRequest,
|
|
5
|
+
ExpressResponse,
|
|
6
|
+
NextFunction,
|
|
7
|
+
} from "../../../Server/Utils/Express";
|
|
8
|
+
import Response from "../../../Server/Utils/Response";
|
|
9
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
10
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
11
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
12
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
13
|
+
import Permission, {
|
|
14
|
+
UserPermission,
|
|
15
|
+
UserTenantAccessPermission,
|
|
16
|
+
} from "../../../Types/Permission";
|
|
17
|
+
import TelemetryType from "../../../Types/Telemetry/TelemetryType";
|
|
18
|
+
import SecurityEvent from "../../../Models/AnalyticsModels/SecurityEvent";
|
|
19
|
+
import UserType from "../../../Types/UserType";
|
|
20
|
+
import {
|
|
21
|
+
afterEach,
|
|
22
|
+
beforeAll,
|
|
23
|
+
beforeEach,
|
|
24
|
+
describe,
|
|
25
|
+
expect,
|
|
26
|
+
jest,
|
|
27
|
+
test,
|
|
28
|
+
} from "@jest/globals";
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* ---------------------------------------------------------------------------
|
|
32
|
+
* /telemetry/security-events/get-attributes[-values]
|
|
33
|
+
* ---------------------------------------------------------------------------
|
|
34
|
+
*
|
|
35
|
+
* These back the "Add Attribute Column" search on the security events table:
|
|
36
|
+
* the keys inside an event's `attributes` map differ per event class and per
|
|
37
|
+
* source, so the UI has to ask which ones exist.
|
|
38
|
+
*
|
|
39
|
+
* They do not go through BaseAnalyticsAPI, so nothing downstream re-checks
|
|
40
|
+
* authorization - the tenantId arrives in a caller-controlled header and
|
|
41
|
+
* UserMiddleware lets tokenless requests through as Public. The guard on the
|
|
42
|
+
* route IS the access control, and it has to stay exactly as permissive as
|
|
43
|
+
* the SecurityEvent model's own read list, no more. That parity is the main
|
|
44
|
+
* thing pinned here.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
type RouterFunction = (
|
|
48
|
+
req: ExpressRequest,
|
|
49
|
+
res: ExpressResponse,
|
|
50
|
+
next: NextFunction,
|
|
51
|
+
) => void | Promise<void>;
|
|
52
|
+
|
|
53
|
+
type RecordedRoute = {
|
|
54
|
+
method: string;
|
|
55
|
+
uri: string;
|
|
56
|
+
handlers: Array<RouterFunction>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const recordedRoutes: Array<RecordedRoute> = [];
|
|
60
|
+
|
|
61
|
+
type RecordRouteFunction = (
|
|
62
|
+
method: string,
|
|
63
|
+
) => (uri: string, ...handlers: Array<RouterFunction>) => void;
|
|
64
|
+
|
|
65
|
+
const recordRoute: RecordRouteFunction = (method: string) => {
|
|
66
|
+
return (uri: string, ...handlers: Array<RouterFunction>): void => {
|
|
67
|
+
recordedRoutes.push({
|
|
68
|
+
method: method.toUpperCase(),
|
|
69
|
+
uri: uri,
|
|
70
|
+
handlers: handlers,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const telemetryRouter: JSONObject = {
|
|
76
|
+
get: recordRoute("get"),
|
|
77
|
+
post: recordRoute("post"),
|
|
78
|
+
put: recordRoute("put"),
|
|
79
|
+
delete: recordRoute("delete"),
|
|
80
|
+
} as unknown as JSONObject;
|
|
81
|
+
|
|
82
|
+
jest.mock("../../../Server/Utils/Express", () => {
|
|
83
|
+
return {
|
|
84
|
+
getRouter: () => {
|
|
85
|
+
return telemetryRouter;
|
|
86
|
+
},
|
|
87
|
+
getClientIp: () => {
|
|
88
|
+
return "203.0.113.7";
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
94
|
+
return {
|
|
95
|
+
sendEntityArrayResponse: jest.fn(),
|
|
96
|
+
sendJsonObjectResponse: jest.fn(),
|
|
97
|
+
sendEmptySuccessResponse: jest.fn(),
|
|
98
|
+
sendEntityResponse: jest.fn(),
|
|
99
|
+
sendErrorResponse: jest.fn(),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const KEYS_ROUTE: string = "/telemetry/security-events/get-attributes";
|
|
104
|
+
const VALUES_ROUTE: string = "/telemetry/security-events/get-attribute-values";
|
|
105
|
+
|
|
106
|
+
type FindRouteFunction = (uri: string) => RecordedRoute;
|
|
107
|
+
|
|
108
|
+
const findRoute: FindRouteFunction = (uri: string): RecordedRoute => {
|
|
109
|
+
const route: RecordedRoute | undefined = recordedRoutes.find(
|
|
110
|
+
(candidate: RecordedRoute): boolean => {
|
|
111
|
+
return candidate.method === "POST" && candidate.uri === uri;
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!route) {
|
|
116
|
+
throw new Error(`Route not registered: ${uri}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return route;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type CallResult = {
|
|
123
|
+
deniedWith: Exception | undefined;
|
|
124
|
+
thrownToNext: unknown;
|
|
125
|
+
reachedHandler: boolean;
|
|
126
|
+
jsonBody: JSONObject | undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/*
|
|
130
|
+
* Runs a recorded route's middleware chain for real, starting at
|
|
131
|
+
* requireUserAuthentication. Index 0 is getUserMiddleware, whose only job is
|
|
132
|
+
* to populate the session fields these fixtures set directly - running it
|
|
133
|
+
* would need a live session store and prove nothing about authorization.
|
|
134
|
+
*/
|
|
135
|
+
type CallRouteFunction = (data: {
|
|
136
|
+
uri: string;
|
|
137
|
+
request: JSONObject;
|
|
138
|
+
body: JSONObject;
|
|
139
|
+
}) => Promise<CallResult>;
|
|
140
|
+
|
|
141
|
+
const callRoute: CallRouteFunction = async (data: {
|
|
142
|
+
uri: string;
|
|
143
|
+
request: JSONObject;
|
|
144
|
+
body: JSONObject;
|
|
145
|
+
}): Promise<CallResult> => {
|
|
146
|
+
const route: RecordedRoute = findRoute(data.uri);
|
|
147
|
+
|
|
148
|
+
const req: ExpressRequest = {
|
|
149
|
+
...data.request,
|
|
150
|
+
body: data.body,
|
|
151
|
+
params: {},
|
|
152
|
+
query: {},
|
|
153
|
+
headers: { "user-agent": "jest-agent" },
|
|
154
|
+
} as unknown as ExpressRequest;
|
|
155
|
+
|
|
156
|
+
const res: ExpressResponse = {
|
|
157
|
+
setHeader: (): void => {
|
|
158
|
+
// no-op
|
|
159
|
+
},
|
|
160
|
+
send: (): void => {
|
|
161
|
+
// no-op
|
|
162
|
+
},
|
|
163
|
+
status: jest.fn().mockReturnThis(),
|
|
164
|
+
json: jest.fn(),
|
|
165
|
+
} as unknown as ExpressResponse;
|
|
166
|
+
|
|
167
|
+
const outcome: { thrownToNext: unknown; reachedHandler: boolean } = {
|
|
168
|
+
thrownToNext: undefined,
|
|
169
|
+
reachedHandler: false,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
for (let index: number = 1; index < route.handlers.length; index++) {
|
|
173
|
+
const handler: RouterFunction | undefined = route.handlers[index];
|
|
174
|
+
|
|
175
|
+
if (!handler) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (index === route.handlers.length - 1) {
|
|
180
|
+
outcome.reachedHandler = true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const step: { calledNext: boolean } = { calledNext: false };
|
|
184
|
+
|
|
185
|
+
const next: NextFunction = ((error?: unknown): void => {
|
|
186
|
+
step.calledNext = true;
|
|
187
|
+
|
|
188
|
+
if (error) {
|
|
189
|
+
outcome.thrownToNext = error;
|
|
190
|
+
}
|
|
191
|
+
}) as unknown as NextFunction;
|
|
192
|
+
|
|
193
|
+
await handler(req, res, next);
|
|
194
|
+
|
|
195
|
+
// A guard that answered the request itself never calls next.
|
|
196
|
+
if (!step.calledNext) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sendErrorResponse: jest.Mock =
|
|
202
|
+
Response.sendErrorResponse as unknown as jest.Mock;
|
|
203
|
+
const sendJsonObjectResponse: jest.Mock =
|
|
204
|
+
Response.sendJsonObjectResponse as unknown as jest.Mock;
|
|
205
|
+
|
|
206
|
+
const errorCall: Array<unknown> | undefined = sendErrorResponse.mock
|
|
207
|
+
.calls[0] as Array<unknown> | undefined;
|
|
208
|
+
const jsonCall: Array<unknown> | undefined = sendJsonObjectResponse.mock
|
|
209
|
+
.calls[0] as Array<unknown> | undefined;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
deniedWith: errorCall ? (errorCall[2] as Exception) : undefined,
|
|
213
|
+
thrownToNext: outcome.thrownToNext,
|
|
214
|
+
reachedHandler: outcome.reachedHandler,
|
|
215
|
+
jsonBody: jsonCall ? (jsonCall[2] as JSONObject) : undefined,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
type BuildPrincipalFunction = (data: {
|
|
220
|
+
projectId: ObjectID;
|
|
221
|
+
userId: ObjectID;
|
|
222
|
+
permissions: Array<Permission>;
|
|
223
|
+
}) => JSONObject;
|
|
224
|
+
|
|
225
|
+
const buildPrincipal: BuildPrincipalFunction = (data: {
|
|
226
|
+
projectId: ObjectID;
|
|
227
|
+
userId: ObjectID;
|
|
228
|
+
permissions: Array<Permission>;
|
|
229
|
+
}): JSONObject => {
|
|
230
|
+
/*
|
|
231
|
+
* isBlockPermission must be an explicit false: getUserPermissions filters
|
|
232
|
+
* tenant permissions with a strict `=== false`, so undefined would silently
|
|
233
|
+
* drop every permission the fixture grants.
|
|
234
|
+
*/
|
|
235
|
+
const userPermissions: Array<UserPermission> = data.permissions.map(
|
|
236
|
+
(permission: Permission): UserPermission => {
|
|
237
|
+
return {
|
|
238
|
+
_type: "UserPermission",
|
|
239
|
+
permission: permission,
|
|
240
|
+
labelIds: [],
|
|
241
|
+
isBlockPermission: false,
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
247
|
+
_type: "UserTenantAccessPermission",
|
|
248
|
+
projectId: data.projectId,
|
|
249
|
+
permissions: userPermissions,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const permissionMap: Dictionary<UserTenantAccessPermission> = {};
|
|
253
|
+
permissionMap[data.projectId.toString()] = tenantPermission;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
userType: UserType.User,
|
|
257
|
+
tenantId: data.projectId,
|
|
258
|
+
userTenantAccessPermission: permissionMap,
|
|
259
|
+
userAuthorization: { userId: data.userId },
|
|
260
|
+
} as unknown as JSONObject;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
describe("Security event attribute API", () => {
|
|
264
|
+
let projectId: ObjectID;
|
|
265
|
+
let userId: ObjectID;
|
|
266
|
+
|
|
267
|
+
interface ServiceSpy {
|
|
268
|
+
mock: { calls: Array<Array<unknown>> };
|
|
269
|
+
mockResolvedValue: (value: Array<string>) => unknown;
|
|
270
|
+
mockRejectedValue: (value: unknown) => unknown;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let fetchAttributes: ServiceSpy;
|
|
274
|
+
let fetchAttributeValues: ServiceSpy;
|
|
275
|
+
|
|
276
|
+
beforeAll(() => {
|
|
277
|
+
recordedRoutes.length = 0;
|
|
278
|
+
/*
|
|
279
|
+
* Loaded lazily rather than with a top-level import: TelemetryAPI calls
|
|
280
|
+
* Express.getRouter() at module scope, so a static import would run the
|
|
281
|
+
* mock factory before `telemetryRouter` is initialised and die in the
|
|
282
|
+
* temporal dead zone.
|
|
283
|
+
*/
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
285
|
+
require("../../../Server/API/TelemetryAPI");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
beforeEach(() => {
|
|
289
|
+
jest.clearAllMocks();
|
|
290
|
+
|
|
291
|
+
projectId = ObjectID.generate();
|
|
292
|
+
userId = ObjectID.generate();
|
|
293
|
+
|
|
294
|
+
/*
|
|
295
|
+
* CI runs the Common suite with BILLING_ENABLED=true (see test-setup.sh),
|
|
296
|
+
* so getDatabaseCommonInteractionProps resolves the caller's plan through
|
|
297
|
+
* ProjectService.getCurrentPlan. These fixtures use a project that never
|
|
298
|
+
* exists in the database, so the real lookup throws "Project ID is invalid"
|
|
299
|
+
* and the handler's error path swallows the request before it ever reaches
|
|
300
|
+
* the attribute service. Billing is orthogonal to what this file pins, so
|
|
301
|
+
* stub the plan lookup to a benign value and let the rest run for real.
|
|
302
|
+
*/
|
|
303
|
+
jest
|
|
304
|
+
.spyOn(ProjectService, "getCurrentPlan")
|
|
305
|
+
.mockResolvedValue({ plan: null, isSubscriptionUnpaid: false });
|
|
306
|
+
|
|
307
|
+
fetchAttributes = jest.spyOn(
|
|
308
|
+
TelemetryAttributeService,
|
|
309
|
+
"fetchAttributes" as never,
|
|
310
|
+
) as unknown as ServiceSpy;
|
|
311
|
+
fetchAttributes.mockResolvedValue([]);
|
|
312
|
+
|
|
313
|
+
fetchAttributeValues = jest.spyOn(
|
|
314
|
+
TelemetryAttributeService,
|
|
315
|
+
"fetchAttributeValues" as never,
|
|
316
|
+
) as unknown as ServiceSpy;
|
|
317
|
+
fetchAttributeValues.mockResolvedValue([]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
afterEach(() => {
|
|
321
|
+
jest.restoreAllMocks();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("route registration", () => {
|
|
325
|
+
test("both routes exist as POST", () => {
|
|
326
|
+
expect(findRoute(KEYS_ROUTE).uri).toBe(KEYS_ROUTE);
|
|
327
|
+
expect(findRoute(VALUES_ROUTE).uri).toBe(VALUES_ROUTE);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
/*
|
|
331
|
+
* getUserMiddleware, requireUserAuthentication, requirePermission, then
|
|
332
|
+
* the handler. A route registered without the guard chain would still
|
|
333
|
+
* "work" for the UI and be wide open.
|
|
334
|
+
*/
|
|
335
|
+
test("both routes carry a guard chain in front of the handler", () => {
|
|
336
|
+
expect(findRoute(KEYS_ROUTE).handlers.length).toBe(4);
|
|
337
|
+
expect(findRoute(VALUES_ROUTE).handlers.length).toBe(4);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("access control", () => {
|
|
342
|
+
test("an unauthenticated caller is refused", async () => {
|
|
343
|
+
const result: CallResult = await callRoute({
|
|
344
|
+
uri: KEYS_ROUTE,
|
|
345
|
+
request: { userType: UserType.Public, tenantId: projectId },
|
|
346
|
+
body: {},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(result.reachedHandler).toBe(false);
|
|
350
|
+
expect(result.deniedWith).toBeDefined();
|
|
351
|
+
expect(fetchAttributes.mock.calls).toHaveLength(0);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("a project member without any telemetry read permission is refused", async () => {
|
|
355
|
+
const result: CallResult = await callRoute({
|
|
356
|
+
uri: KEYS_ROUTE,
|
|
357
|
+
request: buildPrincipal({
|
|
358
|
+
projectId,
|
|
359
|
+
userId,
|
|
360
|
+
permissions: [Permission.ReadProject],
|
|
361
|
+
}),
|
|
362
|
+
body: {},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(result.reachedHandler).toBe(false);
|
|
366
|
+
expect(result.deniedWith).toBeDefined();
|
|
367
|
+
expect(fetchAttributes.mock.calls).toHaveLength(0);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("ReadSecurityEvent alone is enough", async () => {
|
|
371
|
+
const result: CallResult = await callRoute({
|
|
372
|
+
uri: KEYS_ROUTE,
|
|
373
|
+
request: buildPrincipal({
|
|
374
|
+
projectId,
|
|
375
|
+
userId,
|
|
376
|
+
permissions: [Permission.ReadSecurityEvent],
|
|
377
|
+
}),
|
|
378
|
+
body: {},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(result.reachedHandler).toBe(true);
|
|
382
|
+
expect(result.deniedWith).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
/*
|
|
386
|
+
* The guard has to mirror the model's table-level read list exactly, or
|
|
387
|
+
* these routes end up either stricter or looser than the CRUD API for the
|
|
388
|
+
* same rows. Every permission the model grants a read to is exercised.
|
|
389
|
+
*/
|
|
390
|
+
test.each([
|
|
391
|
+
Permission.ProjectOwner,
|
|
392
|
+
Permission.ProjectAdmin,
|
|
393
|
+
Permission.ProjectMember,
|
|
394
|
+
Permission.Viewer,
|
|
395
|
+
Permission.TelemetryAdmin,
|
|
396
|
+
Permission.TelemetryMember,
|
|
397
|
+
Permission.TelemetryViewer,
|
|
398
|
+
Permission.ReadSecurityEvent,
|
|
399
|
+
])("%s can read attribute keys", async (permission: Permission) => {
|
|
400
|
+
const result: CallResult = await callRoute({
|
|
401
|
+
uri: KEYS_ROUTE,
|
|
402
|
+
request: buildPrincipal({
|
|
403
|
+
projectId,
|
|
404
|
+
userId,
|
|
405
|
+
permissions: [permission],
|
|
406
|
+
}),
|
|
407
|
+
body: {},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(result.reachedHandler).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("the model's read list and the guard have not drifted apart", () => {
|
|
414
|
+
const modelReadPermissions: Array<Permission> =
|
|
415
|
+
new SecurityEvent().getReadPermissions();
|
|
416
|
+
|
|
417
|
+
expect(modelReadPermissions).toEqual(
|
|
418
|
+
expect.arrayContaining([Permission.ReadSecurityEvent]),
|
|
419
|
+
);
|
|
420
|
+
expect(modelReadPermissions).toHaveLength(8);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("the values route is guarded the same way", async () => {
|
|
424
|
+
const result: CallResult = await callRoute({
|
|
425
|
+
uri: VALUES_ROUTE,
|
|
426
|
+
request: { userType: UserType.Public, tenantId: projectId },
|
|
427
|
+
body: { attributeKey: "device.hostname" },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(result.reachedHandler).toBe(false);
|
|
431
|
+
expect(result.deniedWith).toBeDefined();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe("attribute keys", () => {
|
|
436
|
+
type CallKeysFunction = (body?: JSONObject) => Promise<CallResult>;
|
|
437
|
+
|
|
438
|
+
const callKeys: CallKeysFunction = (
|
|
439
|
+
body: JSONObject = {},
|
|
440
|
+
): Promise<CallResult> => {
|
|
441
|
+
return callRoute({
|
|
442
|
+
uri: KEYS_ROUTE,
|
|
443
|
+
request: buildPrincipal({
|
|
444
|
+
projectId,
|
|
445
|
+
userId,
|
|
446
|
+
permissions: [Permission.ReadSecurityEvent],
|
|
447
|
+
}),
|
|
448
|
+
body: body,
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/*
|
|
453
|
+
* The whole point of the new source: asking for Log attributes here would
|
|
454
|
+
* return the wrong project's-worth of keys from the wrong table, and look
|
|
455
|
+
* entirely plausible in the picker.
|
|
456
|
+
*/
|
|
457
|
+
test("asks the attribute service for SECURITY EVENT attributes", async () => {
|
|
458
|
+
await callKeys();
|
|
459
|
+
|
|
460
|
+
expect(fetchAttributes.mock.calls[0]![0]).toMatchObject({
|
|
461
|
+
telemetryType: TelemetryType.SecurityEvent,
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("scopes the lookup to the caller's project", async () => {
|
|
466
|
+
await callKeys();
|
|
467
|
+
|
|
468
|
+
const request: JSONObject = fetchAttributes.mock
|
|
469
|
+
.calls[0]![0] as JSONObject;
|
|
470
|
+
|
|
471
|
+
expect((request["projectId"] as ObjectID).toString()).toBe(
|
|
472
|
+
projectId.toString(),
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("returns the keys under an `attributes` field", async () => {
|
|
477
|
+
fetchAttributes.mockResolvedValue(["class_uid", "device.hostname"]);
|
|
478
|
+
|
|
479
|
+
const result: CallResult = await callKeys();
|
|
480
|
+
|
|
481
|
+
expect(result.jsonBody).toEqual({
|
|
482
|
+
attributes: ["class_uid", "device.hostname"],
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("no keys is an empty list, not an error", async () => {
|
|
487
|
+
const result: CallResult = await callKeys();
|
|
488
|
+
|
|
489
|
+
expect(result.jsonBody).toEqual({ attributes: [] });
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("a service failure goes to the error handler rather than a 200", async () => {
|
|
493
|
+
fetchAttributes.mockRejectedValue(new Error("clickhouse said no"));
|
|
494
|
+
|
|
495
|
+
const result: CallResult = await callKeys();
|
|
496
|
+
|
|
497
|
+
expect(result.thrownToNext).toBeDefined();
|
|
498
|
+
expect(result.jsonBody).toBeUndefined();
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe("attribute values", () => {
|
|
503
|
+
type CallValuesFunction = (body: JSONObject) => Promise<CallResult>;
|
|
504
|
+
|
|
505
|
+
const callValues: CallValuesFunction = (
|
|
506
|
+
body: JSONObject,
|
|
507
|
+
): Promise<CallResult> => {
|
|
508
|
+
return callRoute({
|
|
509
|
+
uri: VALUES_ROUTE,
|
|
510
|
+
request: buildPrincipal({
|
|
511
|
+
projectId,
|
|
512
|
+
userId,
|
|
513
|
+
permissions: [Permission.ReadSecurityEvent],
|
|
514
|
+
}),
|
|
515
|
+
body: body,
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
test("asks for SECURITY EVENT values for the requested key", async () => {
|
|
520
|
+
await callValues({ attributeKey: "device.hostname" });
|
|
521
|
+
|
|
522
|
+
expect(fetchAttributeValues.mock.calls[0]![0]).toMatchObject({
|
|
523
|
+
telemetryType: TelemetryType.SecurityEvent,
|
|
524
|
+
attributeKey: "device.hostname",
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("passes the search text through so the narrowing happens server-side", async () => {
|
|
529
|
+
await callValues({ attributeKey: "device.hostname", searchText: "web" });
|
|
530
|
+
|
|
531
|
+
expect(fetchAttributeValues.mock.calls[0]![0]).toMatchObject({
|
|
532
|
+
searchText: "web",
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("a missing attribute key is a bad request, not an unbounded scan", async () => {
|
|
537
|
+
const result: CallResult = await callValues({});
|
|
538
|
+
|
|
539
|
+
expect(result.deniedWith).toBeDefined();
|
|
540
|
+
expect(fetchAttributeValues.mock.calls).toHaveLength(0);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
/*
|
|
544
|
+
* The body is caller-controlled JSON, so a non-string is not a key.
|
|
545
|
+
* Reading it as one would put a non-string into the SQL parameter.
|
|
546
|
+
*/
|
|
547
|
+
test("a non-string attribute key is rejected", async () => {
|
|
548
|
+
const result: CallResult = await callValues({ attributeKey: 42 });
|
|
549
|
+
|
|
550
|
+
expect(result.deniedWith).toBeDefined();
|
|
551
|
+
expect(fetchAttributeValues.mock.calls).toHaveLength(0);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("returns the values under a `values` field", async () => {
|
|
555
|
+
fetchAttributeValues.mockResolvedValue(["web-1", "web-2"]);
|
|
556
|
+
|
|
557
|
+
const result: CallResult = await callValues({
|
|
558
|
+
attributeKey: "device.hostname",
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
expect(result.jsonBody).toEqual({ values: ["web-1", "web-2"] });
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
@@ -50,6 +50,52 @@ const UNTIMESTAMPED_MIGRATION_NAME: string = "InitialMigration";
|
|
|
50
50
|
|
|
51
51
|
const INITIAL_MIGRATION_TIMESTAMP: number = 1717605043663;
|
|
52
52
|
|
|
53
|
+
/*
|
|
54
|
+
* Timestamps that two migrations already share, and always will.
|
|
55
|
+
*
|
|
56
|
+
* `AddNetworkDeviceAutoImportRule1789100000000` and
|
|
57
|
+
* `AddUserTwoFactorBackupCode1789100000000` were authored in parallel, both
|
|
58
|
+
* picked the same round slot (the hazard the timestamps in this directory are
|
|
59
|
+
* hand-picked rather than wall-clock), and both shipped before anyone noticed.
|
|
60
|
+
*
|
|
61
|
+
* Renaming either one now would re-run it. TypeORM records a migration by
|
|
62
|
+
* name, so a renamed class reads as one that has never executed — and both of
|
|
63
|
+
* these CREATE a table with no IF NOT EXISTS, so the re-run fails against
|
|
64
|
+
* every database that already has it. The collision is therefore permanent
|
|
65
|
+
* history, not a fixable defect, and the assertions below step around it while
|
|
66
|
+
* still failing for a NEW one.
|
|
67
|
+
*
|
|
68
|
+
* The exception names the two classes, so it cannot quietly grow: a third
|
|
69
|
+
* migration on the same stamp is not one of them and fails the duplicate
|
|
70
|
+
* guard, and dropping either migration leaves the exception naming a class
|
|
71
|
+
* that is no longer registered, which fails too.
|
|
72
|
+
*/
|
|
73
|
+
const SHIPPED_DUPLICATE_MIGRATIONS: ReadonlyArray<{
|
|
74
|
+
timestamp: number;
|
|
75
|
+
classNames: ReadonlyArray<string>;
|
|
76
|
+
}> = [
|
|
77
|
+
{
|
|
78
|
+
timestamp: 1789100000000,
|
|
79
|
+
classNames: [
|
|
80
|
+
"AddNetworkDeviceAutoImportRule1789100000000",
|
|
81
|
+
"AddUserTwoFactorBackupCode1789100000000",
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* The exception is written as the exact pair of classes that share the stamp,
|
|
88
|
+
* not as the bare number, so it excuses those two and nothing else. Excusing
|
|
89
|
+
* the number would have excused a THIRD migration landing on it — a brand new
|
|
90
|
+
* collision, waved through by the exception made for the old one.
|
|
91
|
+
*/
|
|
92
|
+
const SHIPPED_DUPLICATE_TIMESTAMPS: ReadonlyArray<number> =
|
|
93
|
+
SHIPPED_DUPLICATE_MIGRATIONS.map(
|
|
94
|
+
(duplicate: { timestamp: number }): number => {
|
|
95
|
+
return duplicate.timestamp;
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
53
99
|
const TIMESTAMPED_FILE: RegExp = new RegExp("^\\d+-");
|
|
54
100
|
|
|
55
101
|
type MigrationClass = new () => MigrationInterface;
|
|
@@ -125,13 +171,58 @@ describe("the migration registry", () => {
|
|
|
125
171
|
return timestamp !== null;
|
|
126
172
|
});
|
|
127
173
|
|
|
128
|
-
const duplicates: Array<number> = timestamps
|
|
129
|
-
(timestamp: number, index: number): boolean => {
|
|
174
|
+
const duplicates: Array<number> = timestamps
|
|
175
|
+
.filter((timestamp: number, index: number): boolean => {
|
|
130
176
|
return timestamps.indexOf(timestamp) !== index;
|
|
177
|
+
})
|
|
178
|
+
/*
|
|
179
|
+
* A collision that has already shipped cannot be undone — see
|
|
180
|
+
* SHIPPED_DUPLICATE_TIMESTAMPS. A new one still fails here, which is
|
|
181
|
+
* the only case this assertion can actually do anything about.
|
|
182
|
+
*/
|
|
183
|
+
.filter((timestamp: number): boolean => {
|
|
184
|
+
return !SHIPPED_DUPLICATE_TIMESTAMPS.includes(timestamp);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(duplicates).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* Keeps the exception list honest. Every entry has to name a collision
|
|
192
|
+
* that is really there, so the day one of those migrations is deleted its
|
|
193
|
+
* entry goes with it — and nobody can quietly widen the list to wave a new
|
|
194
|
+
* collision through, because a wrong entry fails right here.
|
|
195
|
+
*/
|
|
196
|
+
test("grandfathers exactly the collisions that exist, and no more", () => {
|
|
197
|
+
const names: Array<string> = registered.map(
|
|
198
|
+
(migrationClass: MigrationClass): string => {
|
|
199
|
+
return migrationClass.name;
|
|
131
200
|
},
|
|
132
201
|
);
|
|
133
202
|
|
|
134
|
-
|
|
203
|
+
for (const duplicate of SHIPPED_DUPLICATE_MIGRATIONS) {
|
|
204
|
+
const onThisTimestamp: Array<string> = registered
|
|
205
|
+
.filter((migrationClass: MigrationClass): boolean => {
|
|
206
|
+
return timestampOf(migrationClass) === duplicate.timestamp;
|
|
207
|
+
})
|
|
208
|
+
.map((migrationClass: MigrationClass): string => {
|
|
209
|
+
return migrationClass.name;
|
|
210
|
+
})
|
|
211
|
+
.sort();
|
|
212
|
+
|
|
213
|
+
/*
|
|
214
|
+
* Both halves matter. Naming a class that is no longer registered
|
|
215
|
+
* means the exception has outlived the collision and must go; finding
|
|
216
|
+
* a class the exception does not name means a NEW migration has
|
|
217
|
+
* landed on the shipped stamp, which is the very thing the duplicate
|
|
218
|
+
* guard exists to catch.
|
|
219
|
+
*/
|
|
220
|
+
expect(onThisTimestamp).toEqual([...duplicate.classNames].sort());
|
|
221
|
+
|
|
222
|
+
for (const className of duplicate.classNames) {
|
|
223
|
+
expect(names).toContain(className);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
135
226
|
});
|
|
136
227
|
|
|
137
228
|
test("never registers the same migration class twice", () => {
|
|
@@ -168,10 +259,35 @@ describe("the migration registry", () => {
|
|
|
168
259
|
return false;
|
|
169
260
|
}
|
|
170
261
|
|
|
262
|
+
/*
|
|
263
|
+
* One file per registered class, which for every timestamp but the
|
|
264
|
+
* grandfathered collision means exactly one file. For that one it
|
|
265
|
+
* means two — both classes are real, both have a file, and neither can
|
|
266
|
+
* be renamed. See SHIPPED_DUPLICATE_TIMESTAMPS.
|
|
267
|
+
*/
|
|
268
|
+
const grandfathered:
|
|
269
|
+
| { timestamp: number; classNames: ReadonlyArray<string> }
|
|
270
|
+
| undefined = SHIPPED_DUPLICATE_MIGRATIONS.find(
|
|
271
|
+
(duplicate: { timestamp: number }): boolean => {
|
|
272
|
+
return duplicate.timestamp === timestamp;
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
/*
|
|
277
|
+
* One file per registered class, which for every timestamp but the
|
|
278
|
+
* grandfathered collision means exactly one file. For that one it
|
|
279
|
+
* means exactly as many files as the exception names — a fixed
|
|
280
|
+
* number, never the live count, so an unexpected third file on that
|
|
281
|
+
* stamp fails here rather than being counted and excused.
|
|
282
|
+
*/
|
|
283
|
+
const expectedFileCount: number = grandfathered
|
|
284
|
+
? grandfathered.classNames.length
|
|
285
|
+
: 1;
|
|
286
|
+
|
|
171
287
|
return (
|
|
172
288
|
files.filter((file: string): boolean => {
|
|
173
289
|
return file.startsWith(`${timestamp}-`);
|
|
174
|
-
}).length !==
|
|
290
|
+
}).length !== expectedFileCount
|
|
175
291
|
);
|
|
176
292
|
})
|
|
177
293
|
.map((migrationClass: MigrationClass): string => {
|