@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,33 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Detection rules gain correlation semantics (issue #3398):
|
|
5
|
+
* distinctCountField switches what a rule counts — unique values of a
|
|
6
|
+
* field (distinct usernames, distinct source IPs) instead of raw matching
|
|
7
|
+
* events — and matchCountThreshold holds a rule back until a group
|
|
8
|
+
* reaches N in one evaluation window. Threshold defaults to 1 so every
|
|
9
|
+
* existing rule keeps its fire-on-any-match behavior.
|
|
10
|
+
*/
|
|
11
|
+
export class AddDetectionRuleDistinctCountColumns1789512000000
|
|
12
|
+
implements MigrationInterface
|
|
13
|
+
{
|
|
14
|
+
public name: string = "AddDetectionRuleDistinctCountColumns1789512000000";
|
|
15
|
+
|
|
16
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
17
|
+
await queryRunner.query(
|
|
18
|
+
`ALTER TABLE "DetectionRule" ADD "distinctCountField" character varying(100)`,
|
|
19
|
+
);
|
|
20
|
+
await queryRunner.query(
|
|
21
|
+
`ALTER TABLE "DetectionRule" ADD "matchCountThreshold" integer NOT NULL DEFAULT '1'`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
26
|
+
await queryRunner.query(
|
|
27
|
+
`ALTER TABLE "DetectionRule" DROP COLUMN "matchCountThreshold"`,
|
|
28
|
+
);
|
|
29
|
+
await queryRunner.query(
|
|
30
|
+
`ALTER TABLE "DetectionRule" DROP COLUMN "distinctCountField"`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -546,7 +546,10 @@ import { AddUserSlackAndMicrosoftTeams1789000000000 } from "./1789000000000-AddU
|
|
|
546
546
|
import { AddNetworkDeviceAutoImportRule1789100000000 } from "./1789100000000-AddNetworkDeviceAutoImportRule";
|
|
547
547
|
import { AddSysObjectIdPatternToAutoImportRule1789200000000 } from "./1789200000000-AddSysObjectIdPatternToAutoImportRule";
|
|
548
548
|
import { AddAutoApplyVendorHealthTemplate1789300000000 } from "./1789300000000-AddAutoApplyVendorHealthTemplate";
|
|
549
|
+
import { AddCampaignUtmFields1789500000000 } from "./1789500000000-AddCampaignUtmFields";
|
|
549
550
|
import { AddUserTwoFactorBackupCode1789100000000 } from "./1789100000000-AddUserTwoFactorBackupCode";
|
|
551
|
+
import { AddNameToNetworkDeviceDiscoveryScan1789400000000 } from "./1789400000000-AddNameToNetworkDeviceDiscoveryScan";
|
|
552
|
+
import { AddDetectionRuleDistinctCountColumns1789512000000 } from "./1789512000000-AddDetectionRuleDistinctCountColumns";
|
|
550
553
|
import { MigrationName1787142779538 } from "./1787142779538-MigrationName";
|
|
551
554
|
import { MigrationName1787156982416 } from "./1787156982416-MigrationName";
|
|
552
555
|
|
|
@@ -1102,4 +1105,7 @@ export default [
|
|
|
1102
1105
|
AddSysObjectIdPatternToAutoImportRule1789200000000,
|
|
1103
1106
|
AddAutoApplyVendorHealthTemplate1789300000000,
|
|
1104
1107
|
AddUserTwoFactorBackupCode1789100000000,
|
|
1108
|
+
AddNameToNetworkDeviceDiscoveryScan1789400000000,
|
|
1109
|
+
AddCampaignUtmFields1789500000000,
|
|
1110
|
+
AddDetectionRuleDistinctCountColumns1789512000000,
|
|
1105
1111
|
];
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import ServerException from "../../Types/Exception/ServerException";
|
|
2
|
+
import {
|
|
3
|
+
ExpressRequest,
|
|
4
|
+
ExpressResponse,
|
|
5
|
+
NextFunction,
|
|
6
|
+
OneUptimeRequest,
|
|
7
|
+
headerValueToString,
|
|
8
|
+
} from "../Utils/Express";
|
|
9
|
+
import logger, { getLogAttributesFromRequest } from "../Utils/Logger";
|
|
10
|
+
import Response from "../Utils/Response";
|
|
11
|
+
import zlib from "zlib";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Ceilings for the global `Content-Encoding: gzip` request path.
|
|
15
|
+
*
|
|
16
|
+
* Both are 50 MiB, which is exactly what body-parser is given for the
|
|
17
|
+
* identity path (`jsonBodyParserOptions.limit` / `urlEncodedBodyParserOptions.limit`,
|
|
18
|
+
* and `bytes("50mb")` is 1024-based too). Announcing gzip must not buy a
|
|
19
|
+
* caller more room than sending the same body uncompressed.
|
|
20
|
+
*
|
|
21
|
+
* They are two constants because they bound two different things and
|
|
22
|
+
* neither alone is sufficient:
|
|
23
|
+
*
|
|
24
|
+
* - COMPRESSED bounds what arrives on the socket, so a client cannot
|
|
25
|
+
* stream unbounded bytes into the process.
|
|
26
|
+
* - DECOMPRESSED bounds what the inflate produces. gzip of repeated
|
|
27
|
+
* bytes reaches four figures of amplification: 130,479 bytes of gzip
|
|
28
|
+
* inflates to 134,217,728 bytes (1,029x, measured), so the compressed
|
|
29
|
+
* cap on its own leaves ~50 GB of expansion on the table.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_COMPRESSED_REQUEST_BODY_BYTES: number = 50 * 1024 * 1024;
|
|
32
|
+
export const MAX_DECOMPRESSED_REQUEST_BODY_BYTES: number = 50 * 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* Inflate a `Content-Encoding: gzip` request body under hard limits, and
|
|
36
|
+
* hand the decompressed bytes on as `req.body` (a Buffer, which is what
|
|
37
|
+
* this path has always produced).
|
|
38
|
+
*
|
|
39
|
+
* This middleware runs BEFORE routing and therefore before authentication,
|
|
40
|
+
* so every limit here is an unauthenticated-attacker limit.
|
|
41
|
+
*
|
|
42
|
+
* Two implementation notes that are easy to get wrong:
|
|
43
|
+
*
|
|
44
|
+
* 1. It streams. Buffering the whole compressed body and then calling
|
|
45
|
+
* `zlib.gunzip` would hold the compressed cap AND the decompressed cap
|
|
46
|
+
* at once, and would not notice a bomb until the last byte arrived.
|
|
47
|
+
* Feeding a `createGunzip` stream instead trips the cap after ~64 KiB
|
|
48
|
+
* of a 128 MiB bomb (measured), before most of the body is even sent.
|
|
49
|
+
*
|
|
50
|
+
* 2. It counts the output itself. `maxOutputLength` looks like the right
|
|
51
|
+
* knob but Node only honours it on the convenience methods
|
|
52
|
+
* (`zlib.gunzip` / `gunzipSync`); on a `createGunzip` STREAM it is
|
|
53
|
+
* silently ignored - a stream with `maxOutputLength: 1 MiB` happily
|
|
54
|
+
* emits 128 MiB. Verified against Node 24. Setting it here would read
|
|
55
|
+
* as protection while providing none, so the running total below is
|
|
56
|
+
* what actually enforces the ceiling.
|
|
57
|
+
*/
|
|
58
|
+
export default class GzipRequestBodyMiddleware {
|
|
59
|
+
public static parseBody(
|
|
60
|
+
req: ExpressRequest,
|
|
61
|
+
res: ExpressResponse,
|
|
62
|
+
next: NextFunction,
|
|
63
|
+
): void {
|
|
64
|
+
/*
|
|
65
|
+
* Cheap pre-check before a byte is read. Content-Length is advisory (a
|
|
66
|
+
* chunked request carries none) but when it is present and already over
|
|
67
|
+
* the cap there is no reason to accept the body at all.
|
|
68
|
+
*/
|
|
69
|
+
const contentLengthHeader: string | undefined = headerValueToString(
|
|
70
|
+
req.headers["content-length"],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (contentLengthHeader) {
|
|
74
|
+
const declaredLength: number = parseInt(contentLengthHeader, 10);
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
!isNaN(declaredLength) &&
|
|
78
|
+
declaredLength > MAX_COMPRESSED_REQUEST_BODY_BYTES
|
|
79
|
+
) {
|
|
80
|
+
GzipRequestBodyMiddleware.rejectTooLarge(
|
|
81
|
+
req,
|
|
82
|
+
res,
|
|
83
|
+
"compressed",
|
|
84
|
+
declaredLength,
|
|
85
|
+
MAX_COMPRESSED_REQUEST_BODY_BYTES,
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const inflate: zlib.Gunzip = zlib.createGunzip();
|
|
92
|
+
|
|
93
|
+
const decoded: Array<Buffer> = [];
|
|
94
|
+
|
|
95
|
+
let compressedBytes: number = 0;
|
|
96
|
+
let decompressedBytes: number = 0;
|
|
97
|
+
let requestEnded: boolean = false;
|
|
98
|
+
let awaitingDrain: boolean = false;
|
|
99
|
+
let settled: boolean = false;
|
|
100
|
+
|
|
101
|
+
const settle: (finish: () => void) => void = (finish: () => void): void => {
|
|
102
|
+
if (settled) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
settled = true;
|
|
107
|
+
|
|
108
|
+
req.removeListener("data", onData);
|
|
109
|
+
req.removeListener("end", onEnd);
|
|
110
|
+
req.removeListener("error", onRequestError);
|
|
111
|
+
req.removeListener("close", onClose);
|
|
112
|
+
|
|
113
|
+
inflate.removeAllListeners();
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* A stream that emits "error" with no listener throws, and an
|
|
117
|
+
* uncaught throw here would turn a memory-exhaustion fix into a
|
|
118
|
+
* process-exit one. Nothing can consume a post-teardown error, so
|
|
119
|
+
* swallow it explicitly rather than leaving the stream bare.
|
|
120
|
+
*/
|
|
121
|
+
inflate.on("error", (): void => {});
|
|
122
|
+
inflate.destroy();
|
|
123
|
+
|
|
124
|
+
decoded.length = 0;
|
|
125
|
+
|
|
126
|
+
finish();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/*
|
|
130
|
+
* Stop reading rather than draining. The session replay middleware
|
|
131
|
+
* deliberately drains its oversized bodies to keep the keep-alive
|
|
132
|
+
* connection usable for the next chunk, but that is a cooperative
|
|
133
|
+
* first-party client sending a 4 MiB cap's worth of bytes. Here the
|
|
134
|
+
* body we are refusing may be gigabytes from someone who wants us to
|
|
135
|
+
* read them, so we pause: Node flushes the response we already wrote
|
|
136
|
+
* and then closes the connection because the request was never
|
|
137
|
+
* consumed, which is precisely the outcome we want.
|
|
138
|
+
*/
|
|
139
|
+
const stopReading: () => void = (): void => {
|
|
140
|
+
if (typeof req.pause === "function") {
|
|
141
|
+
req.pause();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const onData: (chunk: Buffer | string) => void = (
|
|
146
|
+
chunk: Buffer | string,
|
|
147
|
+
): void => {
|
|
148
|
+
const asBuffer: Buffer = Buffer.isBuffer(chunk)
|
|
149
|
+
? chunk
|
|
150
|
+
: Buffer.from(chunk, "utf-8");
|
|
151
|
+
|
|
152
|
+
compressedBytes += asBuffer.length;
|
|
153
|
+
|
|
154
|
+
if (compressedBytes > MAX_COMPRESSED_REQUEST_BODY_BYTES) {
|
|
155
|
+
settle((): void => {
|
|
156
|
+
stopReading();
|
|
157
|
+
GzipRequestBodyMiddleware.rejectTooLarge(
|
|
158
|
+
req,
|
|
159
|
+
res,
|
|
160
|
+
"compressed",
|
|
161
|
+
compressedBytes,
|
|
162
|
+
MAX_COMPRESSED_REQUEST_BODY_BYTES,
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* Honour the inflate's backpressure. zlib does its work on the
|
|
170
|
+
* threadpool, so a socket that is faster than the inflate will
|
|
171
|
+
* otherwise pile the whole compressed body into the writable queue -
|
|
172
|
+
* which is the unbounded buffering this middleware exists to remove,
|
|
173
|
+
* just one object further down. Pausing also means the caps below get
|
|
174
|
+
* to see the output before we accept the next chunk, so a bomb is
|
|
175
|
+
* refused while most of it is still on the wire.
|
|
176
|
+
*/
|
|
177
|
+
if (!inflate.write(asBuffer) && !awaitingDrain) {
|
|
178
|
+
awaitingDrain = true;
|
|
179
|
+
|
|
180
|
+
if (typeof req.pause === "function") {
|
|
181
|
+
req.pause();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
inflate.once("drain", (): void => {
|
|
185
|
+
awaitingDrain = false;
|
|
186
|
+
|
|
187
|
+
if (!settled && typeof req.resume === "function") {
|
|
188
|
+
req.resume();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const onEnd: () => void = (): void => {
|
|
195
|
+
requestEnded = true;
|
|
196
|
+
|
|
197
|
+
/*
|
|
198
|
+
* A request that announced gzip and then sent nothing. `inflate.end()`
|
|
199
|
+
* on an empty stream raises "unexpected end of file", which used to
|
|
200
|
+
* turn every such request - including a bare GET carrying the header -
|
|
201
|
+
* into a 500 plus an error-level log line. Treat it as the empty body
|
|
202
|
+
* it is and let the route decide.
|
|
203
|
+
*/
|
|
204
|
+
if (compressedBytes === 0) {
|
|
205
|
+
settle((): void => {
|
|
206
|
+
req.body = Buffer.alloc(0);
|
|
207
|
+
next();
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
inflate.end();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const onRequestError: (err: Error) => void = (err: Error): void => {
|
|
216
|
+
settle((): void => {
|
|
217
|
+
next(err);
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/*
|
|
222
|
+
* The client hung up mid-body. There is nobody left to answer, so drop
|
|
223
|
+
* the inflate and everything it is holding instead of waiting for a
|
|
224
|
+
* `end` that will never come. `close` also fires on the happy path,
|
|
225
|
+
* after `end`, which is what `requestEnded` guards.
|
|
226
|
+
*/
|
|
227
|
+
const onClose: () => void = (): void => {
|
|
228
|
+
if (requestEnded) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
settle((): void => {});
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
inflate.on("data", (chunk: Buffer): void => {
|
|
236
|
+
decompressedBytes += chunk.length;
|
|
237
|
+
|
|
238
|
+
if (decompressedBytes > MAX_DECOMPRESSED_REQUEST_BODY_BYTES) {
|
|
239
|
+
settle((): void => {
|
|
240
|
+
stopReading();
|
|
241
|
+
GzipRequestBodyMiddleware.rejectTooLarge(
|
|
242
|
+
req,
|
|
243
|
+
res,
|
|
244
|
+
"decompressed",
|
|
245
|
+
decompressedBytes,
|
|
246
|
+
MAX_DECOMPRESSED_REQUEST_BODY_BYTES,
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
decoded.push(chunk);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
inflate.on("end", (): void => {
|
|
256
|
+
const body: Buffer = Buffer.concat(
|
|
257
|
+
decoded as unknown as Array<Uint8Array>,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
settle((): void => {
|
|
261
|
+
req.body = body;
|
|
262
|
+
next();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
inflate.on("error", (err: Error): void => {
|
|
267
|
+
settle((): void => {
|
|
268
|
+
logger.error(err, getLogAttributesFromRequest(req as OneUptimeRequest));
|
|
269
|
+
|
|
270
|
+
Response.sendErrorResponse(
|
|
271
|
+
req,
|
|
272
|
+
res,
|
|
273
|
+
new ServerException("Error decompressing data"),
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
req.on("data", onData);
|
|
279
|
+
req.on("end", onEnd);
|
|
280
|
+
req.on("error", onRequestError);
|
|
281
|
+
req.on("close", onClose);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private static rejectTooLarge(
|
|
285
|
+
req: ExpressRequest,
|
|
286
|
+
res: ExpressResponse,
|
|
287
|
+
stage: "compressed" | "decompressed",
|
|
288
|
+
bytes: number,
|
|
289
|
+
limit: number,
|
|
290
|
+
): void {
|
|
291
|
+
if (res.headersSent) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
* debug, not warn or error. This path is unauthenticated, so a level
|
|
297
|
+
* that reaches the log sink by default would hand the same attacker a
|
|
298
|
+
* log-amplification vector in place of the memory one - while still
|
|
299
|
+
* leaving an operator something to turn on when a legitimate client
|
|
300
|
+
* starts getting 413s.
|
|
301
|
+
*/
|
|
302
|
+
logger.debug(
|
|
303
|
+
`Rejected a gzip request body: ${stage} size reached ${bytes} bytes, limit is ${limit}.`,
|
|
304
|
+
getLogAttributesFromRequest(req as OneUptimeRequest),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
res.status(413).json({
|
|
308
|
+
message: `Request body too large. The ${stage} body of a gzip-encoded request may not exceed ${limit} bytes.`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -1,17 +1,101 @@
|
|
|
1
|
+
import BadRequestException from "../../Types/Exception/BadRequestException";
|
|
2
|
+
import PayloadTooLargeException from "../../Types/Exception/PayloadTooLargeException";
|
|
3
|
+
import {
|
|
4
|
+
ExpressRequest,
|
|
5
|
+
ExpressResponse,
|
|
6
|
+
NextFunction,
|
|
7
|
+
} from "../Utils/Express";
|
|
1
8
|
import multer from "multer";
|
|
2
9
|
import { RequestHandler } from "express";
|
|
3
10
|
|
|
11
|
+
/*
|
|
12
|
+
* Ceilings for multipart bodies.
|
|
13
|
+
*
|
|
14
|
+
* memoryStorage means every part is buffered in the process, and both
|
|
15
|
+
* routes that mount this middleware run it BEFORE their auth check -
|
|
16
|
+
* Pyroscope ingest before isAuthorizedServiceMiddleware, SendGrid inbound
|
|
17
|
+
* before the webhook secret is verified. So these are the limits an
|
|
18
|
+
* unauthenticated caller is held to, and multer's defaults leave most of
|
|
19
|
+
* them at Infinity: file size, file count, field count and part count are
|
|
20
|
+
* all unbounded out of the box (only fieldSize has a default, 1 MB).
|
|
21
|
+
*
|
|
22
|
+
* The sizes match the 50 MiB body-parser limit the rest of the app uses,
|
|
23
|
+
* and nginx cuts the shipped deployment lower still (50M on
|
|
24
|
+
* /incoming-email, its 1M default on /pyroscope). The COUNTS are the part
|
|
25
|
+
* that was genuinely missing: without them a body of ten thousand
|
|
26
|
+
* one-byte parts costs ten thousand allocations and passes every size
|
|
27
|
+
* check.
|
|
28
|
+
*/
|
|
29
|
+
export const MAX_MULTIPART_FILE_BYTES: number = 50 * 1024 * 1024;
|
|
30
|
+
export const MAX_MULTIPART_FIELD_BYTES: number = 25 * 1024 * 1024;
|
|
31
|
+
export const MAX_MULTIPART_FILES: number = 50;
|
|
32
|
+
export const MAX_MULTIPART_FIELDS: number = 200;
|
|
33
|
+
|
|
4
34
|
/*
|
|
5
35
|
* Configure multer for handling multipart/form-data
|
|
6
36
|
* Uses memory storage to store files in memory as Buffer objects
|
|
7
37
|
*/
|
|
8
|
-
const upload: multer.Multer = multer({
|
|
38
|
+
const upload: multer.Multer = multer({
|
|
39
|
+
storage: multer.memoryStorage(),
|
|
40
|
+
limits: {
|
|
41
|
+
fileSize: MAX_MULTIPART_FILE_BYTES,
|
|
42
|
+
fieldSize: MAX_MULTIPART_FIELD_BYTES,
|
|
43
|
+
files: MAX_MULTIPART_FILES,
|
|
44
|
+
fields: MAX_MULTIPART_FIELDS,
|
|
45
|
+
/*
|
|
46
|
+
* Parts are files + fields, so this has to allow both in full or it
|
|
47
|
+
* would be the effective limit and the two above would never fire.
|
|
48
|
+
*/
|
|
49
|
+
parts: MAX_MULTIPART_FILES + MAX_MULTIPART_FIELDS,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const uploadAny: RequestHandler = upload.any() as unknown as RequestHandler;
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* The LIMIT_* codes that mean "you sent too much". LIMIT_UNEXPECTED_FILE
|
|
57
|
+
* is deliberately absent: it means the body named a field the route did
|
|
58
|
+
* not ask for, which is a malformed request rather than an oversized one.
|
|
59
|
+
* (`.any()` accepts every field name, so it should not arise here - the
|
|
60
|
+
* distinction is kept so this stays correct if the mount ever narrows.)
|
|
61
|
+
*/
|
|
62
|
+
const TOO_LARGE_CODES: Set<string> = new Set<string>([
|
|
63
|
+
"LIMIT_PART_COUNT",
|
|
64
|
+
"LIMIT_FILE_SIZE",
|
|
65
|
+
"LIMIT_FILE_COUNT",
|
|
66
|
+
"LIMIT_FIELD_KEY",
|
|
67
|
+
"LIMIT_FIELD_VALUE",
|
|
68
|
+
"LIMIT_FIELD_COUNT",
|
|
69
|
+
]);
|
|
9
70
|
|
|
10
71
|
/*
|
|
11
72
|
* Middleware for handling any file uploads (multipart/form-data)
|
|
12
73
|
* This is useful for webhooks that send data as multipart/form-data (e.g., SendGrid inbound email)
|
|
74
|
+
*
|
|
75
|
+
* Wrapped so a limit breach answers 413 rather than 500. multer signals
|
|
76
|
+
* every limit with a MulterError whose `code` is one of the LIMIT_*
|
|
77
|
+
* values, and nothing downstream knows what those mean - the generic
|
|
78
|
+
* error handler would turn a caller's oversized upload into a server
|
|
79
|
+
* error and page whoever is on call for it.
|
|
13
80
|
*/
|
|
14
|
-
const MultipartFormDataMiddleware: RequestHandler =
|
|
15
|
-
|
|
81
|
+
const MultipartFormDataMiddleware: RequestHandler = (
|
|
82
|
+
req: ExpressRequest,
|
|
83
|
+
res: ExpressResponse,
|
|
84
|
+
next: NextFunction,
|
|
85
|
+
): void => {
|
|
86
|
+
uploadAny(req, res, (err: unknown): void => {
|
|
87
|
+
if (err instanceof multer.MulterError) {
|
|
88
|
+
const message: string = `Multipart request rejected: ${err.code}.`;
|
|
89
|
+
|
|
90
|
+
return next(
|
|
91
|
+
TOO_LARGE_CODES.has(err.code)
|
|
92
|
+
? new PayloadTooLargeException(message)
|
|
93
|
+
: new BadRequestException(message),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return next(err);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
16
100
|
|
|
17
101
|
export default MultipartFormDataMiddleware;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ExpressResponse,
|
|
3
|
+
headerValueToString,
|
|
3
4
|
NextFunction,
|
|
4
5
|
OneUptimeRequest,
|
|
5
6
|
} from "../Utils/Express";
|
|
7
|
+
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
6
8
|
import Response from "../Utils/Response";
|
|
7
9
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
10
|
+
import ServiceUnavailableException from "../../Types/Exception/ServiceUnavailableException";
|
|
8
11
|
import { SlackAppSigningSecret } from "../EnvironmentConfig";
|
|
9
12
|
import crypto from "crypto";
|
|
10
13
|
import logger, { getLogAttributesFromRequest } from "../Utils/Logger";
|
|
@@ -21,6 +24,9 @@ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
|
21
24
|
*/
|
|
22
25
|
const SIGNATURE_PREFIX: string = "v0=";
|
|
23
26
|
const HEX_DIGEST_REGEX: RegExp = /^[a-f0-9]{64}$/i;
|
|
27
|
+
const UNIX_TIMESTAMP_REGEX: RegExp = /^\d+$/;
|
|
28
|
+
const MAX_REQUEST_AGE_IN_SECONDS: number = 5 * 60;
|
|
29
|
+
const REPLAY_CACHE_NAMESPACE: string = "slack-request-replay";
|
|
24
30
|
|
|
25
31
|
export default class SlackAuthorization {
|
|
26
32
|
@CaptureSpan()
|
|
@@ -51,25 +57,41 @@ export default class SlackAuthorization {
|
|
|
51
57
|
// validate slack signing secret
|
|
52
58
|
const slackSigningSecret: string = SlackAppSigningSecret.toString();
|
|
53
59
|
|
|
54
|
-
const slackSignature: string | undefined =
|
|
55
|
-
"x-slack-signature"
|
|
56
|
-
|
|
57
|
-
const timestamp: string =
|
|
58
|
-
"x-slack-request-timestamp"
|
|
59
|
-
] as string;
|
|
60
|
+
const slackSignature: string | undefined = headerValueToString(
|
|
61
|
+
req.headers["x-slack-signature"],
|
|
62
|
+
);
|
|
63
|
+
const timestamp: string =
|
|
64
|
+
headerValueToString(req.headers["x-slack-request-timestamp"]) || "";
|
|
60
65
|
// Use rawBody for both JSON and URL-encoded requests, fallback to rawFormUrlEncodedBody for backward compatibility
|
|
61
66
|
const requestBody: string =
|
|
62
67
|
(req as OneUptimeRequest).rawBody ||
|
|
63
68
|
(req as OneUptimeRequest).rawFormUrlEncodedBody ||
|
|
64
69
|
"";
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
/*
|
|
72
|
+
* Slack signs the timestamp specifically so a captured request cannot be
|
|
73
|
+
* replayed indefinitely. Verify the documented five-minute freshness
|
|
74
|
+
* window before doing any signature work. The strict decimal check keeps
|
|
75
|
+
* Number parsing from accepting partial values such as "123abc".
|
|
76
|
+
*/
|
|
77
|
+
const timestampInSeconds: number = Number(timestamp);
|
|
78
|
+
const nowInSeconds: number = Math.floor(Date.now() / 1000);
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
!UNIX_TIMESTAMP_REGEX.test(timestamp) ||
|
|
82
|
+
!Number.isSafeInteger(timestampInSeconds) ||
|
|
83
|
+
Math.abs(nowInSeconds - timestampInSeconds) > MAX_REQUEST_AGE_IN_SECONDS
|
|
84
|
+
) {
|
|
85
|
+
logger.error(
|
|
86
|
+
"Slack request has a missing, malformed, or stale X-Slack-Request-Timestamp header.",
|
|
87
|
+
getLogAttributesFromRequest(req),
|
|
88
|
+
);
|
|
89
|
+
return Response.sendErrorResponse(
|
|
90
|
+
req,
|
|
91
|
+
res,
|
|
92
|
+
new BadDataException("Slack Signature Verification Failed."),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
73
95
|
|
|
74
96
|
const providedDigest: string =
|
|
75
97
|
slackSignature && slackSignature.startsWith(SIGNATURE_PREFIX)
|
|
@@ -94,11 +116,6 @@ export default class SlackAuthorization {
|
|
|
94
116
|
.update(baseString)
|
|
95
117
|
.digest("hex");
|
|
96
118
|
|
|
97
|
-
logger.debug(
|
|
98
|
-
`Generated signature: ${SIGNATURE_PREFIX}${expectedDigest}`,
|
|
99
|
-
getLogAttributesFromRequest(req),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
119
|
/*
|
|
103
120
|
* Both sides are decoded from 64 validated hex characters, so both
|
|
104
121
|
* buffers are 32 bytes and timingSafeEqual cannot throw here.
|
|
@@ -120,6 +137,67 @@ export default class SlackAuthorization {
|
|
|
120
137
|
);
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
/*
|
|
141
|
+
* URL verification and options loading have no side effects and need their
|
|
142
|
+
* normal response on every valid delivery. All other signed Slack routes
|
|
143
|
+
* are processed at most once.
|
|
144
|
+
*/
|
|
145
|
+
const isNaturallyIdempotentRequest: boolean =
|
|
146
|
+
req.route?.path === "/slack/options-load" ||
|
|
147
|
+
req.body?.["type"] === "url_verification";
|
|
148
|
+
|
|
149
|
+
if (isNaturallyIdempotentRequest) {
|
|
150
|
+
next();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
* Freshness bounds replay to a small window; this atomic cache claim closes
|
|
156
|
+
* that remaining window across every application replica. The expiry lasts
|
|
157
|
+
* until this timestamp can no longer pass the freshness check. That can be
|
|
158
|
+
* almost ten minutes when a sender clock is five minutes ahead.
|
|
159
|
+
*/
|
|
160
|
+
const replayCacheTtlInSeconds: number = Math.max(
|
|
161
|
+
timestampInSeconds + MAX_REQUEST_AGE_IN_SECONDS - nowInSeconds + 1,
|
|
162
|
+
1,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
let isFirstDelivery: boolean;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
isFirstDelivery = await GlobalCache.setStringIfNotExists(
|
|
169
|
+
REPLAY_CACHE_NAMESPACE,
|
|
170
|
+
expectedDigest,
|
|
171
|
+
timestamp,
|
|
172
|
+
{ expiresInSeconds: replayCacheTtlInSeconds },
|
|
173
|
+
);
|
|
174
|
+
} catch {
|
|
175
|
+
/*
|
|
176
|
+
* Replay protection is part of authentication, so cache failure must
|
|
177
|
+
* fail closed. Letting the request through would silently restore the
|
|
178
|
+
* vulnerability whenever Redis is unavailable.
|
|
179
|
+
*/
|
|
180
|
+
logger.error(
|
|
181
|
+
"Slack replay protection is unavailable.",
|
|
182
|
+
getLogAttributesFromRequest(req),
|
|
183
|
+
);
|
|
184
|
+
return Response.sendErrorResponse(
|
|
185
|
+
req,
|
|
186
|
+
res,
|
|
187
|
+
new ServiceUnavailableException(
|
|
188
|
+
"Slack request verification is temporarily unavailable.",
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!isFirstDelivery) {
|
|
194
|
+
logger.debug(
|
|
195
|
+
"Duplicate Slack request acknowledged without reprocessing.",
|
|
196
|
+
getLogAttributesFromRequest(req),
|
|
197
|
+
);
|
|
198
|
+
return Response.sendTextResponse(req, res, "");
|
|
199
|
+
}
|
|
200
|
+
|
|
123
201
|
logger.debug(
|
|
124
202
|
"Slack request authorized successfully",
|
|
125
203
|
getLogAttributesFromRequest(req),
|