@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,298 @@
|
|
|
1
|
+
import { describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
import timers from "timers";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* multer calls `setImmediate` internally (make-middleware.js and
|
|
6
|
+
* remove-uploaded-files.js), and Common's jest environment is jsdom, which
|
|
7
|
+
* does not expose it. A `@jest-environment node` docblock is not an option
|
|
8
|
+
* here - Common's shared jest.setup.ts touches `window`, which is why
|
|
9
|
+
* EsbuildConfig.test.ts spawns a subprocess instead - so lend jsdom the
|
|
10
|
+
* real one from Node.
|
|
11
|
+
*/
|
|
12
|
+
if (
|
|
13
|
+
typeof (globalThis as unknown as { setImmediate?: unknown }).setImmediate !==
|
|
14
|
+
"function"
|
|
15
|
+
) {
|
|
16
|
+
(globalThis as unknown as { setImmediate: unknown }).setImmediate =
|
|
17
|
+
timers.setImmediate;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* The limits under test are 25 and 50 MiB, so the fixtures that probe them
|
|
22
|
+
* are 25 and 50 MiB of real multipart body parsed by the real busboy.
|
|
23
|
+
* That does not fit in jest's 5s default.
|
|
24
|
+
*/
|
|
25
|
+
jest.setTimeout(180_000);
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* The multipart reader used by Pyroscope ingest and the SendGrid inbound
|
|
29
|
+
* email webhook.
|
|
30
|
+
*
|
|
31
|
+
* Both routes mount it BEFORE their auth check - Pyroscope before
|
|
32
|
+
* isAuthorizedServiceMiddleware, SendGrid before the webhook secret is
|
|
33
|
+
* verified - and it uses multer's memoryStorage, so every part it accepts
|
|
34
|
+
* is buffered in the process on behalf of a caller who has not proved
|
|
35
|
+
* anything yet.
|
|
36
|
+
*
|
|
37
|
+
* multer's defaults leave almost all of that unbounded: fileSize, files,
|
|
38
|
+
* fields and parts are all Infinity out of the box, and only fieldSize
|
|
39
|
+
* has a default at all. These tests pin the limits and the status code a
|
|
40
|
+
* breach produces, because the failure mode of getting this wrong is
|
|
41
|
+
* silent - an unbounded upload looks exactly like a working one until the
|
|
42
|
+
* pod dies.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import MultipartFormDataMiddleware, {
|
|
46
|
+
MAX_MULTIPART_FIELDS,
|
|
47
|
+
MAX_MULTIPART_FIELD_BYTES,
|
|
48
|
+
MAX_MULTIPART_FILES,
|
|
49
|
+
MAX_MULTIPART_FILE_BYTES,
|
|
50
|
+
} from "../../../Server/Middleware/MultipartFormData";
|
|
51
|
+
import {
|
|
52
|
+
ExpressRequest,
|
|
53
|
+
ExpressResponse,
|
|
54
|
+
NextFunction,
|
|
55
|
+
} from "../../../Server/Utils/Express";
|
|
56
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
57
|
+
import ExceptionCode from "../../../Types/Exception/ExceptionCode";
|
|
58
|
+
import { Readable } from "stream";
|
|
59
|
+
|
|
60
|
+
const BOUNDARY: string = "----oneuptimetestboundary";
|
|
61
|
+
|
|
62
|
+
interface MultipartPart {
|
|
63
|
+
name: string;
|
|
64
|
+
value: Buffer;
|
|
65
|
+
filename?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildMultipartBody(parts: Array<MultipartPart>): Buffer {
|
|
69
|
+
const chunks: Array<Buffer> = [];
|
|
70
|
+
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
const disposition: string = part.filename
|
|
73
|
+
? `form-data; name="${part.name}"; filename="${part.filename}"`
|
|
74
|
+
: `form-data; name="${part.name}"`;
|
|
75
|
+
|
|
76
|
+
const header: string =
|
|
77
|
+
`--${BOUNDARY}\r\n` +
|
|
78
|
+
`Content-Disposition: ${disposition}\r\n` +
|
|
79
|
+
(part.filename ? "Content-Type: application/octet-stream\r\n" : "") +
|
|
80
|
+
"\r\n";
|
|
81
|
+
|
|
82
|
+
chunks.push(Buffer.from(header));
|
|
83
|
+
chunks.push(part.value);
|
|
84
|
+
chunks.push(Buffer.from("\r\n"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
chunks.push(Buffer.from(`--${BOUNDARY}--\r\n`));
|
|
88
|
+
|
|
89
|
+
return Buffer.concat(chunks as unknown as Array<Uint8Array>);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface Outcome {
|
|
93
|
+
req: ExpressRequest;
|
|
94
|
+
error: unknown;
|
|
95
|
+
nextCalls: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* Drive the middleware with a real multipart body over a real Readable, so
|
|
100
|
+
* busboy does the parsing it does in production rather than being faked.
|
|
101
|
+
*/
|
|
102
|
+
function run(parts: Array<MultipartPart>): Promise<Outcome> {
|
|
103
|
+
const body: Buffer = buildMultipartBody(parts);
|
|
104
|
+
|
|
105
|
+
const req: Readable & {
|
|
106
|
+
headers: Record<string, string>;
|
|
107
|
+
body?: unknown;
|
|
108
|
+
files?: unknown;
|
|
109
|
+
} = Readable.from([body]) as never;
|
|
110
|
+
|
|
111
|
+
req.headers = {
|
|
112
|
+
"content-type": `multipart/form-data; boundary=${BOUNDARY}`,
|
|
113
|
+
"content-length": String(body.length),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const res: ExpressResponse = {
|
|
117
|
+
status: (): unknown => {
|
|
118
|
+
return { send: (): void => {}, json: (): void => {} };
|
|
119
|
+
},
|
|
120
|
+
headersSent: false,
|
|
121
|
+
} as unknown as ExpressResponse;
|
|
122
|
+
|
|
123
|
+
return new Promise<Outcome>((resolve: (value: Outcome) => void): void => {
|
|
124
|
+
let nextCalls: number = 0;
|
|
125
|
+
|
|
126
|
+
MultipartFormDataMiddleware(req as unknown as ExpressRequest, res, ((
|
|
127
|
+
err?: unknown,
|
|
128
|
+
): void => {
|
|
129
|
+
nextCalls++;
|
|
130
|
+
resolve({
|
|
131
|
+
req: req as unknown as ExpressRequest,
|
|
132
|
+
error: err,
|
|
133
|
+
nextCalls: nextCalls,
|
|
134
|
+
});
|
|
135
|
+
}) as NextFunction);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function files(outcome: Outcome): Array<{ fieldname: string; buffer: Buffer }> {
|
|
140
|
+
return (outcome.req.files || []) as Array<{
|
|
141
|
+
fieldname: string;
|
|
142
|
+
buffer: Buffer;
|
|
143
|
+
}>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("MultipartFormData - the limits are set where they were reasoned about", () => {
|
|
147
|
+
test("sizes match the app-wide 50 MiB body-parser number", () => {
|
|
148
|
+
expect(MAX_MULTIPART_FILE_BYTES).toBe(50 * 1024 * 1024);
|
|
149
|
+
expect(MAX_MULTIPART_FIELD_BYTES).toBe(25 * 1024 * 1024);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("counts are bounded at all, which is the part multer leaves at Infinity", () => {
|
|
153
|
+
expect(MAX_MULTIPART_FILES).toBe(50);
|
|
154
|
+
expect(MAX_MULTIPART_FIELDS).toBe(200);
|
|
155
|
+
expect(Number.isFinite(MAX_MULTIPART_FILES)).toBe(true);
|
|
156
|
+
expect(Number.isFinite(MAX_MULTIPART_FIELDS)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("MultipartFormData - normal bodies still parse", () => {
|
|
161
|
+
test("a single file arrives as a Buffer on req.files", async () => {
|
|
162
|
+
const outcome: Outcome = await run([
|
|
163
|
+
{ name: "profile", value: Buffer.from("pprof-bytes"), filename: "p.pb" },
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
expect(outcome.error).toBeUndefined();
|
|
167
|
+
expect(files(outcome)).toHaveLength(1);
|
|
168
|
+
expect(files(outcome)[0]!.fieldname).toBe("profile");
|
|
169
|
+
expect(files(outcome)[0]!.buffer.toString()).toBe("pprof-bytes");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("fields and files together, the SendGrid shape", async () => {
|
|
173
|
+
const outcome: Outcome = await run([
|
|
174
|
+
{ name: "from", value: Buffer.from("someone@example.com") },
|
|
175
|
+
{ name: "subject", value: Buffer.from("hello") },
|
|
176
|
+
{ name: "attachment1", value: Buffer.from("PDF"), filename: "a.pdf" },
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
expect(outcome.error).toBeUndefined();
|
|
180
|
+
expect((outcome.req.body as Record<string, string>)["from"]).toBe(
|
|
181
|
+
"someone@example.com",
|
|
182
|
+
);
|
|
183
|
+
expect(files(outcome)).toHaveLength(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("a body with no parts at all is fine", async () => {
|
|
187
|
+
const outcome: Outcome = await run([]);
|
|
188
|
+
|
|
189
|
+
expect(outcome.error).toBeUndefined();
|
|
190
|
+
expect(files(outcome)).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a file right at the count limit is accepted", async () => {
|
|
194
|
+
const outcome: Outcome = await run(
|
|
195
|
+
Array.from({ length: MAX_MULTIPART_FILES }, (_v: unknown, i: number) => {
|
|
196
|
+
return {
|
|
197
|
+
name: `f${i}`,
|
|
198
|
+
value: Buffer.from("x"),
|
|
199
|
+
filename: `f${i}.bin`,
|
|
200
|
+
};
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(outcome.error).toBeUndefined();
|
|
205
|
+
expect(files(outcome)).toHaveLength(MAX_MULTIPART_FILES);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("fields right at the count limit are accepted", async () => {
|
|
209
|
+
const outcome: Outcome = await run(
|
|
210
|
+
Array.from({ length: MAX_MULTIPART_FIELDS }, (_v: unknown, i: number) => {
|
|
211
|
+
return { name: `k${i}`, value: Buffer.from("v") };
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(outcome.error).toBeUndefined();
|
|
216
|
+
expect(
|
|
217
|
+
Object.keys(outcome.req.body as Record<string, unknown>),
|
|
218
|
+
).toHaveLength(MAX_MULTIPART_FIELDS);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("MultipartFormData - breaches answer 413, not 500", () => {
|
|
223
|
+
test("one file over the size limit", async () => {
|
|
224
|
+
const outcome: Outcome = await run([
|
|
225
|
+
{
|
|
226
|
+
name: "big",
|
|
227
|
+
value: Buffer.alloc(MAX_MULTIPART_FILE_BYTES + 1),
|
|
228
|
+
filename: "big.bin",
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
expect(outcome.error).toBeInstanceOf(Exception);
|
|
233
|
+
expect((outcome.error as Exception).code).toBe(
|
|
234
|
+
ExceptionCode.PayloadTooLargeException,
|
|
235
|
+
);
|
|
236
|
+
expect((outcome.error as Exception).code).toBe(413);
|
|
237
|
+
expect((outcome.error as Exception).message).toContain("LIMIT_FILE_SIZE");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("one file too many", async () => {
|
|
241
|
+
const outcome: Outcome = await run(
|
|
242
|
+
Array.from(
|
|
243
|
+
{ length: MAX_MULTIPART_FILES + 1 },
|
|
244
|
+
(_v: unknown, i: number) => {
|
|
245
|
+
return {
|
|
246
|
+
name: `f${i}`,
|
|
247
|
+
value: Buffer.from("x"),
|
|
248
|
+
filename: `f${i}.bin`,
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect((outcome.error as Exception).code).toBe(
|
|
255
|
+
ExceptionCode.PayloadTooLargeException,
|
|
256
|
+
);
|
|
257
|
+
expect((outcome.error as Exception).message).toContain("LIMIT_FILE_COUNT");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("one field too many - thousands of tiny parts is the cheap version of this attack", async () => {
|
|
261
|
+
const outcome: Outcome = await run(
|
|
262
|
+
Array.from(
|
|
263
|
+
{ length: MAX_MULTIPART_FIELDS + 1 },
|
|
264
|
+
(_v: unknown, i: number) => {
|
|
265
|
+
return { name: `k${i}`, value: Buffer.from("v") };
|
|
266
|
+
},
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect((outcome.error as Exception).code).toBe(
|
|
271
|
+
ExceptionCode.PayloadTooLargeException,
|
|
272
|
+
);
|
|
273
|
+
expect((outcome.error as Exception).message).toContain("LIMIT_FIELD_COUNT");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("one field value over the size limit", async () => {
|
|
277
|
+
const outcome: Outcome = await run([
|
|
278
|
+
{ name: "html", value: Buffer.alloc(MAX_MULTIPART_FIELD_BYTES + 1) },
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
expect((outcome.error as Exception).code).toBe(
|
|
282
|
+
ExceptionCode.PayloadTooLargeException,
|
|
283
|
+
);
|
|
284
|
+
expect((outcome.error as Exception).message).toContain("LIMIT_FIELD_VALUE");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("next() is called exactly once on a breach", async () => {
|
|
288
|
+
const outcome: Outcome = await run([
|
|
289
|
+
{
|
|
290
|
+
name: "big",
|
|
291
|
+
value: Buffer.alloc(MAX_MULTIPART_FILE_BYTES + 1),
|
|
292
|
+
filename: "big.bin",
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
expect(outcome.nextCalls).toBe(1);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe, expect, test
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
2
2
|
import crypto from "crypto";
|
|
3
|
+
import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
|
|
3
4
|
import SlackAuthorization from "../../../Server/Middleware/SlackAuthorization";
|
|
4
5
|
import Response from "../../../Server/Utils/Response";
|
|
5
6
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
OneUptimeRequest,
|
|
9
10
|
} from "../../../Server/Utils/Express";
|
|
10
11
|
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
12
|
+
import ServiceUnavailableException from "../../../Types/Exception/ServiceUnavailableException";
|
|
11
13
|
|
|
12
14
|
const SIGNING_SECRET: string = "slack-app-signing-secret";
|
|
13
15
|
|
|
@@ -28,6 +30,16 @@ jest.mock("../../../Server/Utils/Response", () => {
|
|
|
28
30
|
__esModule: true,
|
|
29
31
|
default: {
|
|
30
32
|
sendErrorResponse: jest.fn(),
|
|
33
|
+
sendTextResponse: jest.fn(),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
jest.mock("../../../Server/Infrastructure/GlobalCache", () => {
|
|
39
|
+
return {
|
|
40
|
+
__esModule: true,
|
|
41
|
+
default: {
|
|
42
|
+
setStringIfNotExists: jest.fn(),
|
|
31
43
|
},
|
|
32
44
|
};
|
|
33
45
|
});
|
|
@@ -41,11 +53,16 @@ jest.mock("../../../Server/Utils/Response", () => {
|
|
|
41
53
|
* have to come back as the intended 400.
|
|
42
54
|
*/
|
|
43
55
|
describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
|
|
44
|
-
const
|
|
56
|
+
const NOW_IN_SECONDS: number = 1_800_000_000;
|
|
57
|
+
const TIMESTAMP: string = NOW_IN_SECONDS.toString();
|
|
45
58
|
const RAW_BODY: string = "token=abc&team_id=T123&command=%2Foneuptime";
|
|
46
59
|
|
|
47
60
|
const sendErrorResponse: jest.Mock =
|
|
48
61
|
Response.sendErrorResponse as unknown as jest.Mock;
|
|
62
|
+
const sendTextResponse: jest.Mock =
|
|
63
|
+
Response.sendTextResponse as unknown as jest.Mock;
|
|
64
|
+
const setStringIfNotExists: jest.Mock =
|
|
65
|
+
GlobalCache.setStringIfNotExists as unknown as jest.Mock;
|
|
49
66
|
|
|
50
67
|
const res: ExpressResponse = {} as ExpressResponse;
|
|
51
68
|
let next: NextFunction;
|
|
@@ -62,15 +79,19 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
|
|
|
62
79
|
type BuildRequestFunction = (
|
|
63
80
|
signature: string | undefined,
|
|
64
81
|
body?: string,
|
|
82
|
+
timestamp?: string | null,
|
|
65
83
|
) => OneUptimeRequest;
|
|
66
84
|
|
|
67
85
|
const buildRequest: BuildRequestFunction = (
|
|
68
86
|
signature: string | undefined,
|
|
69
87
|
body: string = RAW_BODY,
|
|
88
|
+
timestamp: string | null = TIMESTAMP,
|
|
70
89
|
): OneUptimeRequest => {
|
|
71
|
-
const headers: Record<string, string> = {
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
const headers: Record<string, string> = {};
|
|
91
|
+
|
|
92
|
+
if (timestamp !== null) {
|
|
93
|
+
headers["x-slack-request-timestamp"] = timestamp;
|
|
94
|
+
}
|
|
74
95
|
|
|
75
96
|
if (signature !== undefined) {
|
|
76
97
|
headers["x-slack-signature"] = signature;
|
|
@@ -81,9 +102,15 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
|
|
|
81
102
|
|
|
82
103
|
beforeEach(() => {
|
|
83
104
|
jest.clearAllMocks();
|
|
105
|
+
jest.spyOn(Date, "now").mockReturnValue(NOW_IN_SECONDS * 1000);
|
|
106
|
+
setStringIfNotExists.mockResolvedValue(true);
|
|
84
107
|
next = jest.fn();
|
|
85
108
|
});
|
|
86
109
|
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
jest.restoreAllMocks();
|
|
112
|
+
});
|
|
113
|
+
|
|
87
114
|
test("calls next when the signature matches the timestamp and body", async () => {
|
|
88
115
|
const req: OneUptimeRequest = buildRequest(sign(TIMESTAMP, RAW_BODY));
|
|
89
116
|
|
|
@@ -91,6 +118,12 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
|
|
|
91
118
|
|
|
92
119
|
expect(next).toHaveBeenCalled();
|
|
93
120
|
expect(sendErrorResponse).not.toHaveBeenCalled();
|
|
121
|
+
expect(setStringIfNotExists).toHaveBeenCalledWith(
|
|
122
|
+
"slack-request-replay",
|
|
123
|
+
sign(TIMESTAMP, RAW_BODY).slice("v0=".length),
|
|
124
|
+
TIMESTAMP,
|
|
125
|
+
{ expiresInSeconds: 5 * 60 + 1 },
|
|
126
|
+
);
|
|
94
127
|
});
|
|
95
128
|
|
|
96
129
|
test("rejects a well-formed signature computed over a different body", async () => {
|
|
@@ -107,6 +140,212 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
|
|
|
107
140
|
res,
|
|
108
141
|
new BadDataException("Slack Signature Verification Failed."),
|
|
109
142
|
);
|
|
143
|
+
expect(setStringIfNotExists).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("accepts a request exactly five minutes old", async () => {
|
|
147
|
+
const timestamp: string = (NOW_IN_SECONDS - 5 * 60).toString();
|
|
148
|
+
const req: OneUptimeRequest = buildRequest(
|
|
149
|
+
sign(timestamp, RAW_BODY),
|
|
150
|
+
RAW_BODY,
|
|
151
|
+
timestamp,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
|
|
155
|
+
|
|
156
|
+
expect(next).toHaveBeenCalled();
|
|
157
|
+
expect(sendErrorResponse).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("accepts a request exactly five minutes in the future and covers the full replay window", async () => {
|
|
161
|
+
const timestamp: string = (NOW_IN_SECONDS + 5 * 60).toString();
|
|
162
|
+
const req: OneUptimeRequest = buildRequest(
|
|
163
|
+
sign(timestamp, RAW_BODY),
|
|
164
|
+
RAW_BODY,
|
|
165
|
+
timestamp,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
|
|
169
|
+
|
|
170
|
+
expect(next).toHaveBeenCalled();
|
|
171
|
+
expect(setStringIfNotExists).toHaveBeenCalledWith(
|
|
172
|
+
"slack-request-replay",
|
|
173
|
+
sign(timestamp, RAW_BODY).slice("v0=".length),
|
|
174
|
+
timestamp,
|
|
175
|
+
{ expiresInSeconds: 10 * 60 + 1 },
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("rejects a correctly signed request older than five minutes", async () => {
|
|
180
|
+
const timestamp: string = (NOW_IN_SECONDS - 5 * 60 - 1).toString();
|
|
181
|
+
const req: OneUptimeRequest = buildRequest(
|
|
182
|
+
sign(timestamp, RAW_BODY),
|
|
183
|
+
RAW_BODY,
|
|
184
|
+
timestamp,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
|
|
188
|
+
|
|
189
|
+
expect(next).not.toHaveBeenCalled();
|
|
190
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
191
|
+
req,
|
|
192
|
+
res,
|
|
193
|
+
new BadDataException("Slack Signature Verification Failed."),
|
|
194
|
+
);
|
|
195
|
+
expect(setStringIfNotExists).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("rejects a correctly signed request more than five minutes in the future", async () => {
|
|
199
|
+
const timestamp: string = (NOW_IN_SECONDS + 5 * 60 + 1).toString();
|
|
200
|
+
const req: OneUptimeRequest = buildRequest(
|
|
201
|
+
sign(timestamp, RAW_BODY),
|
|
202
|
+
RAW_BODY,
|
|
203
|
+
timestamp,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
|
|
207
|
+
|
|
208
|
+
expect(next).not.toHaveBeenCalled();
|
|
209
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
210
|
+
req,
|
|
211
|
+
res,
|
|
212
|
+
new BadDataException("Slack Signature Verification Failed."),
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("acknowledges a duplicate without invoking the Slack handler", async () => {
|
|
217
|
+
setStringIfNotExists.mockResolvedValue(false);
|
|
218
|
+
const req: OneUptimeRequest = buildRequest(sign(TIMESTAMP, RAW_BODY));
|
|
219
|
+
|
|
220
|
+
await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
|
|
221
|
+
|
|
222
|
+
expect(next).not.toHaveBeenCalled();
|
|
223
|
+
expect(sendErrorResponse).not.toHaveBeenCalled();
|
|
224
|
+
expect(sendTextResponse).toHaveBeenCalledWith(req, res, "");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("uses the canonical digest so signature casing cannot bypass replay detection", async () => {
|
|
228
|
+
const lowercaseSignature: string = sign(TIMESTAMP, RAW_BODY);
|
|
229
|
+
const uppercaseSignature: string = `v0=${lowercaseSignature
|
|
230
|
+
.slice("v0=".length)
|
|
231
|
+
.toUpperCase()}`;
|
|
232
|
+
const firstRequest: OneUptimeRequest = buildRequest(lowercaseSignature);
|
|
233
|
+
const replayRequest: OneUptimeRequest = buildRequest(uppercaseSignature);
|
|
234
|
+
|
|
235
|
+
setStringIfNotExists
|
|
236
|
+
.mockResolvedValueOnce(true)
|
|
237
|
+
.mockResolvedValueOnce(false);
|
|
238
|
+
|
|
239
|
+
await SlackAuthorization.isAuthorizedSlackRequest(firstRequest, res, next);
|
|
240
|
+
await SlackAuthorization.isAuthorizedSlackRequest(replayRequest, res, next);
|
|
241
|
+
|
|
242
|
+
const canonicalDigest: string = lowercaseSignature.slice("v0=".length);
|
|
243
|
+
expect(setStringIfNotExists).toHaveBeenNthCalledWith(
|
|
244
|
+
1,
|
|
245
|
+
"slack-request-replay",
|
|
246
|
+
canonicalDigest,
|
|
247
|
+
TIMESTAMP,
|
|
248
|
+
{ expiresInSeconds: 5 * 60 + 1 },
|
|
249
|
+
);
|
|
250
|
+
expect(setStringIfNotExists).toHaveBeenNthCalledWith(
|
|
251
|
+
2,
|
|
252
|
+
"slack-request-replay",
|
|
253
|
+
canonicalDigest,
|
|
254
|
+
TIMESTAMP,
|
|
255
|
+
{ expiresInSeconds: 5 * 60 + 1 },
|
|
256
|
+
);
|
|
257
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
258
|
+
expect(sendTextResponse).toHaveBeenCalledWith(replayRequest, res, "");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("allows repeated URL verification requests to reach their idempotent handler", async () => {
|
|
262
|
+
const signature: string = sign(TIMESTAMP, RAW_BODY);
|
|
263
|
+
const firstRequest: OneUptimeRequest = buildRequest(signature);
|
|
264
|
+
const secondRequest: OneUptimeRequest = buildRequest(signature);
|
|
265
|
+
|
|
266
|
+
Object.assign(firstRequest, {
|
|
267
|
+
route: { path: "/slack/events" },
|
|
268
|
+
body: { type: "url_verification" },
|
|
269
|
+
});
|
|
270
|
+
Object.assign(secondRequest, {
|
|
271
|
+
route: { path: "/slack/events" },
|
|
272
|
+
body: { type: "url_verification" },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await SlackAuthorization.isAuthorizedSlackRequest(firstRequest, res, next);
|
|
276
|
+
await SlackAuthorization.isAuthorizedSlackRequest(secondRequest, res, next);
|
|
277
|
+
|
|
278
|
+
expect(next).toHaveBeenCalledTimes(2);
|
|
279
|
+
expect(setStringIfNotExists).not.toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("allows repeated options loads to reach their idempotent handler", async () => {
|
|
283
|
+
const signature: string = sign(TIMESTAMP, RAW_BODY);
|
|
284
|
+
const firstRequest: OneUptimeRequest = buildRequest(signature);
|
|
285
|
+
const secondRequest: OneUptimeRequest = buildRequest(signature);
|
|
286
|
+
|
|
287
|
+
Object.assign(firstRequest, {
|
|
288
|
+
route: { path: "/slack/options-load" },
|
|
289
|
+
});
|
|
290
|
+
Object.assign(secondRequest, {
|
|
291
|
+
route: { path: "/slack/options-load" },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await SlackAuthorization.isAuthorizedSlackRequest(firstRequest, res, next);
|
|
295
|
+
await SlackAuthorization.isAuthorizedSlackRequest(secondRequest, res, next);
|
|
296
|
+
|
|
297
|
+
expect(next).toHaveBeenCalledTimes(2);
|
|
298
|
+
expect(setStringIfNotExists).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("fails closed when distributed replay protection is unavailable", async () => {
|
|
302
|
+
setStringIfNotExists.mockRejectedValue(new Error("cache unavailable"));
|
|
303
|
+
const req: OneUptimeRequest = buildRequest(sign(TIMESTAMP, RAW_BODY));
|
|
304
|
+
|
|
305
|
+
await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
|
|
306
|
+
|
|
307
|
+
expect(next).not.toHaveBeenCalled();
|
|
308
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
309
|
+
req,
|
|
310
|
+
res,
|
|
311
|
+
new ServiceUnavailableException(
|
|
312
|
+
"Slack request verification is temporarily unavailable.",
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const malformedTimestamps: Array<[string, string | null]> = [
|
|
318
|
+
["absent", null],
|
|
319
|
+
["empty", ""],
|
|
320
|
+
["non-numeric", "not-a-timestamp"],
|
|
321
|
+
["partially numeric", `${TIMESTAMP}abc`],
|
|
322
|
+
["fractional", `${TIMESTAMP}.5`],
|
|
323
|
+
["negative", "-1"],
|
|
324
|
+
["outside the safe integer range", "999999999999999999999"],
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
describe("rejects a malformed timestamp", () => {
|
|
328
|
+
test.each(malformedTimestamps)(
|
|
329
|
+
"%s",
|
|
330
|
+
async (_label: string, timestamp: string | null) => {
|
|
331
|
+
const signedTimestamp: string = timestamp || TIMESTAMP;
|
|
332
|
+
const req: OneUptimeRequest = buildRequest(
|
|
333
|
+
sign(signedTimestamp, RAW_BODY),
|
|
334
|
+
RAW_BODY,
|
|
335
|
+
timestamp,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
await SlackAuthorization.isAuthorizedSlackRequest(req, res, next);
|
|
339
|
+
|
|
340
|
+
expect(next).not.toHaveBeenCalled();
|
|
341
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
342
|
+
req,
|
|
343
|
+
res,
|
|
344
|
+
new BadDataException("Slack Signature Verification Failed."),
|
|
345
|
+
);
|
|
346
|
+
expect(setStringIfNotExists).not.toHaveBeenCalled();
|
|
347
|
+
},
|
|
348
|
+
);
|
|
110
349
|
});
|
|
111
350
|
|
|
112
351
|
/*
|
|
@@ -143,7 +382,25 @@ describe("SlackAuthorization.isAuthorizedSlackRequest", () => {
|
|
|
143
382
|
res,
|
|
144
383
|
new BadDataException("Slack Signature Verification Failed."),
|
|
145
384
|
);
|
|
385
|
+
expect(setStringIfNotExists).not.toHaveBeenCalled();
|
|
146
386
|
},
|
|
147
387
|
);
|
|
148
388
|
});
|
|
389
|
+
|
|
390
|
+
test("normalizes an array signature header without throwing", async () => {
|
|
391
|
+
const req: OneUptimeRequest = buildRequest(undefined);
|
|
392
|
+
req.headers["x-slack-signature"] = ["not-a-signature"];
|
|
393
|
+
|
|
394
|
+
await expect(
|
|
395
|
+
SlackAuthorization.isAuthorizedSlackRequest(req, res, next),
|
|
396
|
+
).resolves.toBeUndefined();
|
|
397
|
+
|
|
398
|
+
expect(next).not.toHaveBeenCalled();
|
|
399
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
400
|
+
req,
|
|
401
|
+
res,
|
|
402
|
+
new BadDataException("Slack Signature Verification Failed."),
|
|
403
|
+
);
|
|
404
|
+
expect(setStringIfNotExists).not.toHaveBeenCalled();
|
|
405
|
+
});
|
|
149
406
|
});
|