@oneuptime/common 12.0.23 → 12.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/DetectionRule.ts +55 -0
- package/Models/DatabaseModels/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/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/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 +130 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
- package/Server/Views/Partials/Head.ejs +2 -1
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
- package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
- package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
- package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
- package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
- package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
- package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
- package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
- package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
- package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
- package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
- package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
- package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
- package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
- package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
- package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
- package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
- package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
- package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
- package/Tests/Server/Services/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 +368 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
- package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
- package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
- package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
- package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
- package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
- package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
- package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
- package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
- package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
- package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
- package/Tests/UI/Components/SideOver.test.tsx +1 -1
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
- package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
- package/Tests/UI/Components/Toggle.test.tsx +25 -0
- package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
- package/Tests/Utils/NetworkDiscovery/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 +472 -0
- package/Types/Code/YamlSyntax.ts +396 -0
- package/Types/Exception/ExceptionCode.ts +1 -0
- package/Types/Exception/PayloadTooLargeException.ts +8 -0
- package/Types/Marketing/Attribution.ts +10 -0
- package/Types/Marketing/MarketingEvent.ts +4 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
- package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
- package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
- package/Types/Telemetry/LlmMetricConventions.ts +124 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
- package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
- package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
- package/UI/Components/Detail/Detail.tsx +10 -0
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
- package/UI/Components/Forms/Fields/FormField.tsx +31 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
- package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
- package/UI/Components/Forms/Validation.ts +46 -0
- package/UI/Components/Input/Input.tsx +3 -0
- package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
- package/UI/Components/ModelTable/Column.ts +8 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
- package/UI/Components/SideOver/SideOver.tsx +5 -5
- package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
- package/UI/Components/StatusPage/ResourceList.tsx +197 -38
- package/UI/Components/Toggle/Toggle.tsx +9 -1
- package/UI/Components/Types/FieldType.ts +1 -0
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
- package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
- package/Utils/StatusPage/ResourceExplorer.ts +258 -0
- package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/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/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/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 +102 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Types/Code/YamlSyntax.js +212 -0
- package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
- package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
- package/build/dist/Types/Marketing/Attribution.js +10 -0
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
- package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +9 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +27 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
- package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Types/FieldType.js +1 -0
- package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/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 +119 -0
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/Tests/UI/Components/Toast.test.tsx +0 -197
- package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
- package/UI/Components/Toast/Toast.tsx +0 -189
- package/UI/Components/Toast/ToastInit.tsx +0 -110
- package/build/dist/UI/Components/Toast/Toast.js +0 -123
- package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
- package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The global `Content-Encoding: gzip` request reader (GHSA-cp58-wc9q-qv53).
|
|
5
|
+
*
|
|
6
|
+
* This middleware runs in StartServer BEFORE routing, so it sees every
|
|
7
|
+
* request to every service on every path, authenticated or not. It used to
|
|
8
|
+
* accumulate the whole compressed body in an unbounded array and hand it to
|
|
9
|
+
* an unbounded `zlib.gunzip` - a decompression bomb requiring no work at all
|
|
10
|
+
* from the attacker: the fixture below turns ~51 KB of anonymous request
|
|
11
|
+
* into 50 MiB of resident Buffer, and the trick scales linearly.
|
|
12
|
+
*
|
|
13
|
+
* There are two ceilings, and most of the tests here exist to show that
|
|
14
|
+
* neither one subsumes the other:
|
|
15
|
+
*
|
|
16
|
+
* - Drop the DECOMPRESSED cap and 51 KB buys 50 MiB.
|
|
17
|
+
* - Drop the COMPRESSED cap and 51 MiB of concatenated EMPTY gzip members -
|
|
18
|
+
* which inflate to exactly zero bytes, so the decompressed cap can never
|
|
19
|
+
* fire - streams straight into the process.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
jest.mock("../../../Server/Utils/Logger", () => {
|
|
23
|
+
return {
|
|
24
|
+
__esModule: true,
|
|
25
|
+
default: {
|
|
26
|
+
debug: jest.fn(),
|
|
27
|
+
info: jest.fn(),
|
|
28
|
+
warn: jest.fn(),
|
|
29
|
+
error: jest.fn(),
|
|
30
|
+
},
|
|
31
|
+
getLogAttributesFromRequest: jest.fn().mockReturnValue({}),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
36
|
+
return {
|
|
37
|
+
__esModule: true,
|
|
38
|
+
default: {
|
|
39
|
+
sendErrorResponse: jest.fn(),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
import GzipRequestBodyMiddleware, {
|
|
45
|
+
MAX_COMPRESSED_REQUEST_BODY_BYTES,
|
|
46
|
+
MAX_DECOMPRESSED_REQUEST_BODY_BYTES,
|
|
47
|
+
} from "../../../Server/Middleware/GzipRequestBody";
|
|
48
|
+
import Response from "../../../Server/Utils/Response";
|
|
49
|
+
import logger from "../../../Server/Utils/Logger";
|
|
50
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
51
|
+
import ExceptionCode from "../../../Types/Exception/ExceptionCode";
|
|
52
|
+
import {
|
|
53
|
+
ExpressRequest,
|
|
54
|
+
ExpressResponse,
|
|
55
|
+
NextFunction,
|
|
56
|
+
} from "../../../Server/Utils/Express";
|
|
57
|
+
import { EventEmitter } from "events";
|
|
58
|
+
import fs from "fs";
|
|
59
|
+
import path from "path";
|
|
60
|
+
import zlib from "zlib";
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* The caps under test are 50 MiB, so the fixtures that probe them are 50 MiB
|
|
64
|
+
* of real gzip and real inflation. That does not fit in jest's 5s default.
|
|
65
|
+
*/
|
|
66
|
+
jest.setTimeout(300_000);
|
|
67
|
+
|
|
68
|
+
type JestMock = {
|
|
69
|
+
mock: { calls: Array<Array<unknown>> };
|
|
70
|
+
mockClear: () => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const sendErrorResponseMock: JestMock =
|
|
74
|
+
Response.sendErrorResponse as unknown as JestMock;
|
|
75
|
+
|
|
76
|
+
function loggerMock(level: "debug" | "warn" | "error"): JestMock {
|
|
77
|
+
return logger[level] as unknown as JestMock;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* Response.sendErrorResponse is a module mock, so the drivers below cannot
|
|
82
|
+
* each attach to it. One shared implementation fans out to whichever driver
|
|
83
|
+
* is currently waiting.
|
|
84
|
+
*/
|
|
85
|
+
const errorResponseListeners: Set<() => void> = new Set();
|
|
86
|
+
|
|
87
|
+
(
|
|
88
|
+
Response.sendErrorResponse as unknown as {
|
|
89
|
+
mockImplementation: (fn: () => void) => void;
|
|
90
|
+
}
|
|
91
|
+
).mockImplementation((): void => {
|
|
92
|
+
for (const listener of Array.from(errorResponseListeners)) {
|
|
93
|
+
listener();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* A request that models the one property this middleware depends on: a
|
|
99
|
+
* paused socket stops delivering. The driver waits while `paused` is set, so
|
|
100
|
+
* backpressure is exercised rather than ignored.
|
|
101
|
+
*/
|
|
102
|
+
interface FakeRequest extends EventEmitter {
|
|
103
|
+
body?: unknown;
|
|
104
|
+
headers: Record<string, string>;
|
|
105
|
+
destroy: () => void;
|
|
106
|
+
pause: () => void;
|
|
107
|
+
resume: () => void;
|
|
108
|
+
destroyed: boolean;
|
|
109
|
+
paused: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface FakeResponse {
|
|
113
|
+
statusCode: number;
|
|
114
|
+
headersSent: boolean;
|
|
115
|
+
jsonBody: unknown;
|
|
116
|
+
status: (code: number) => FakeResponse;
|
|
117
|
+
json: (body: unknown) => FakeResponse;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildResponse(onJson?: () => void): FakeResponse {
|
|
121
|
+
const res: FakeResponse = {
|
|
122
|
+
statusCode: 0,
|
|
123
|
+
headersSent: false,
|
|
124
|
+
jsonBody: undefined,
|
|
125
|
+
status: (code: number): FakeResponse => {
|
|
126
|
+
res.statusCode = code;
|
|
127
|
+
return res;
|
|
128
|
+
},
|
|
129
|
+
json: (body: unknown): FakeResponse => {
|
|
130
|
+
res.jsonBody = body;
|
|
131
|
+
res.headersSent = true;
|
|
132
|
+
|
|
133
|
+
if (onJson) {
|
|
134
|
+
onJson();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return res;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return res;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Let anything that WOULD have happened, happen, before asserting it did not. */
|
|
145
|
+
function quietPeriod(): Promise<void> {
|
|
146
|
+
return new Promise<void>((resolve: () => void): void => {
|
|
147
|
+
setTimeout(resolve, 100);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* Deliberately event-driven rather than a poll loop. The inflate runs on the
|
|
153
|
+
* threadpool, and under jsdom a `setTimeout(0)` poll competes with its
|
|
154
|
+
* callbacks for the event loop - polling turned a 50 MiB inflate into half a
|
|
155
|
+
* minute. The 250 ms timer is only a safety net for a wake-up that never
|
|
156
|
+
* arrives.
|
|
157
|
+
*/
|
|
158
|
+
class Driver {
|
|
159
|
+
public req: FakeRequest;
|
|
160
|
+
public res: FakeResponse;
|
|
161
|
+
public nextCalls: Array<unknown> = [];
|
|
162
|
+
public fedBytes: number = 0;
|
|
163
|
+
|
|
164
|
+
private waiters: Array<() => void> = [];
|
|
165
|
+
private errorResponsesAtStart: number;
|
|
166
|
+
|
|
167
|
+
private wake: () => void = (): void => {
|
|
168
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
169
|
+
waiter();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
public constructor(headers?: Record<string, string>) {
|
|
174
|
+
const req: FakeRequest = new EventEmitter() as FakeRequest;
|
|
175
|
+
|
|
176
|
+
req.headers = headers || {};
|
|
177
|
+
req.destroyed = false;
|
|
178
|
+
req.paused = false;
|
|
179
|
+
req.destroy = (): void => {
|
|
180
|
+
req.destroyed = true;
|
|
181
|
+
};
|
|
182
|
+
req.pause = (): void => {
|
|
183
|
+
req.paused = true;
|
|
184
|
+
};
|
|
185
|
+
req.resume = (): void => {
|
|
186
|
+
req.paused = false;
|
|
187
|
+
this.wake();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
this.req = req;
|
|
191
|
+
this.res = buildResponse(this.wake);
|
|
192
|
+
this.errorResponsesAtStart = sendErrorResponseMock.mock.calls.length;
|
|
193
|
+
|
|
194
|
+
errorResponseListeners.add(this.wake);
|
|
195
|
+
|
|
196
|
+
GzipRequestBodyMiddleware.parseBody(
|
|
197
|
+
this.req as unknown as ExpressRequest,
|
|
198
|
+
this.res as unknown as ExpressResponse,
|
|
199
|
+
((err?: unknown): void => {
|
|
200
|
+
this.nextCalls.push(err);
|
|
201
|
+
this.wake();
|
|
202
|
+
}) as NextFunction,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public isSettled(): boolean {
|
|
207
|
+
return (
|
|
208
|
+
this.nextCalls.length > 0 ||
|
|
209
|
+
this.res.statusCode !== 0 ||
|
|
210
|
+
sendErrorResponseMock.mock.calls.length > this.errorResponsesAtStart
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public errorResponse(): Exception | undefined {
|
|
215
|
+
const calls: Array<Array<unknown>> = sendErrorResponseMock.mock.calls.slice(
|
|
216
|
+
this.errorResponsesAtStart,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return calls.length > 0 ? (calls[0]![2] as Exception) : undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public async waitForOutcome(timeoutMs: number = 120000): Promise<boolean> {
|
|
223
|
+
const startedAt: number = Date.now();
|
|
224
|
+
|
|
225
|
+
while (!this.isSettled()) {
|
|
226
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await this.waitForWake();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
errorResponseListeners.delete(this.wake);
|
|
234
|
+
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Feed chunks the way a socket would: stop while paused, stop once answered. */
|
|
239
|
+
public async feed(chunks: Array<Buffer>): Promise<void> {
|
|
240
|
+
for (const chunk of chunks) {
|
|
241
|
+
while (this.req.paused && !this.isSettled()) {
|
|
242
|
+
await this.waitForWake();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (this.isSettled()) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.req.emit("data", chunk);
|
|
250
|
+
this.fedBytes += chunk.length;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public end(): void {
|
|
255
|
+
this.req.emit("end");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private waitForWake(): Promise<void> {
|
|
259
|
+
return new Promise<void>((resolve: () => void): void => {
|
|
260
|
+
this.waiters.push(resolve);
|
|
261
|
+
setTimeout(resolve, 250);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function gzip(buffer: Buffer): Buffer {
|
|
267
|
+
return zlib.gzipSync(buffer as unknown as Uint8Array);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function chunked(buffer: Buffer, size: number): Array<Buffer> {
|
|
271
|
+
const out: Array<Buffer> = [];
|
|
272
|
+
|
|
273
|
+
for (let offset: number = 0; offset < buffer.length; offset += size) {
|
|
274
|
+
out.push(buffer.subarray(offset, offset + size));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/*
|
|
281
|
+
* A block of concatenated EMPTY gzip members. Each member is 20 valid bytes
|
|
282
|
+
* that inflate to nothing, so a stream of these is arbitrarily large on the
|
|
283
|
+
* wire and exactly zero bytes of output - the shape that makes a
|
|
284
|
+
* decompressed-only cap useless.
|
|
285
|
+
*/
|
|
286
|
+
function emptyMemberBlock(approximateBytes: number): Buffer {
|
|
287
|
+
const member: Buffer = gzip(Buffer.alloc(0));
|
|
288
|
+
const count: number = Math.floor(approximateBytes / member.length);
|
|
289
|
+
|
|
290
|
+
return Buffer.concat(
|
|
291
|
+
Array.from({ length: count }, (): Uint8Array => {
|
|
292
|
+
return member as unknown as Uint8Array;
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Built once - the 50 MiB fixtures are the slow part of this suite. */
|
|
298
|
+
const oneMiBOfEmptyMembers: Buffer = emptyMemberBlock(1024 * 1024);
|
|
299
|
+
|
|
300
|
+
describe("GzipRequestBodyMiddleware - the happy path still works", () => {
|
|
301
|
+
test("inflates a gzip body and hands it on as a Buffer", async () => {
|
|
302
|
+
const payload: Buffer = Buffer.from(
|
|
303
|
+
JSON.stringify({ hello: "world", n: 42 }),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const driver: Driver = new Driver();
|
|
307
|
+
|
|
308
|
+
await driver.feed([gzip(payload)]);
|
|
309
|
+
driver.end();
|
|
310
|
+
|
|
311
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
312
|
+
|
|
313
|
+
expect(driver.nextCalls).toEqual([undefined]);
|
|
314
|
+
expect(Buffer.isBuffer(driver.req.body)).toBe(true);
|
|
315
|
+
expect((driver.req.body as Buffer).toString()).toBe(payload.toString());
|
|
316
|
+
expect(driver.res.statusCode).toBe(0);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("reassembles a body split across many socket-sized chunks", async () => {
|
|
320
|
+
const payload: Buffer = Buffer.from("x".repeat(300_000));
|
|
321
|
+
|
|
322
|
+
const driver: Driver = new Driver();
|
|
323
|
+
|
|
324
|
+
await driver.feed(chunked(gzip(payload), 512));
|
|
325
|
+
driver.end();
|
|
326
|
+
|
|
327
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
328
|
+
|
|
329
|
+
expect(driver.nextCalls).toHaveLength(1);
|
|
330
|
+
expect((driver.req.body as Buffer).length).toBe(payload.length);
|
|
331
|
+
expect((driver.req.body as Buffer).equals(payload as never)).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("a body exactly ON the decompressed limit is accepted", async () => {
|
|
335
|
+
const payload: Buffer = Buffer.alloc(MAX_DECOMPRESSED_REQUEST_BODY_BYTES);
|
|
336
|
+
|
|
337
|
+
const driver: Driver = new Driver();
|
|
338
|
+
|
|
339
|
+
await driver.feed(chunked(gzip(payload), 16 * 1024));
|
|
340
|
+
driver.end();
|
|
341
|
+
|
|
342
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
343
|
+
|
|
344
|
+
expect(driver.res.statusCode).toBe(0);
|
|
345
|
+
expect(driver.nextCalls).toHaveLength(1);
|
|
346
|
+
expect((driver.req.body as Buffer).length).toBe(
|
|
347
|
+
MAX_DECOMPRESSED_REQUEST_BODY_BYTES,
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("a declared Content-Length under the cap is not treated as a rejection", async () => {
|
|
352
|
+
const compressed: Buffer = gzip(Buffer.from("small"));
|
|
353
|
+
|
|
354
|
+
const driver: Driver = new Driver({
|
|
355
|
+
"content-length": String(compressed.length),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await driver.feed([compressed]);
|
|
359
|
+
driver.end();
|
|
360
|
+
|
|
361
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
362
|
+
|
|
363
|
+
expect(driver.res.statusCode).toBe(0);
|
|
364
|
+
expect(driver.nextCalls).toHaveLength(1);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("a garbage Content-Length is ignored rather than trusted", async () => {
|
|
368
|
+
const driver: Driver = new Driver({ "content-length": "not-a-number" });
|
|
369
|
+
|
|
370
|
+
await driver.feed([gzip(Buffer.from("ok"))]);
|
|
371
|
+
driver.end();
|
|
372
|
+
|
|
373
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
374
|
+
|
|
375
|
+
expect(driver.res.statusCode).toBe(0);
|
|
376
|
+
expect(driver.nextCalls).toHaveLength(1);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("GzipRequestBodyMiddleware - decompression bombs (GHSA-cp58-wc9q-qv53)", () => {
|
|
381
|
+
test("one byte over the decompressed cap is a 413, and next() never runs", async () => {
|
|
382
|
+
const payload: Buffer = Buffer.alloc(
|
|
383
|
+
MAX_DECOMPRESSED_REQUEST_BODY_BYTES + 1,
|
|
384
|
+
);
|
|
385
|
+
const compressed: Buffer = gzip(payload);
|
|
386
|
+
|
|
387
|
+
/* The amplification the advisory is about, measured on the fixture. */
|
|
388
|
+
expect(compressed.length).toBeLessThan(payload.length / 500);
|
|
389
|
+
|
|
390
|
+
const driver: Driver = new Driver();
|
|
391
|
+
|
|
392
|
+
await driver.feed(chunked(compressed, 16 * 1024));
|
|
393
|
+
driver.end();
|
|
394
|
+
|
|
395
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
396
|
+
|
|
397
|
+
expect(driver.res.statusCode).toBe(413);
|
|
398
|
+
expect(driver.nextCalls).toHaveLength(0);
|
|
399
|
+
expect(driver.req.body).toBeUndefined();
|
|
400
|
+
expect(
|
|
401
|
+
String((driver.res.jsonBody as { message: string }).message),
|
|
402
|
+
).toContain("decompressed");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("the bomb is refused mid-stream, not after the last byte arrives", async () => {
|
|
406
|
+
/*
|
|
407
|
+
* 64 MiB of output hidden in ~65 KB, followed by 4 MiB of padding that
|
|
408
|
+
* inflates to nothing. The 50 MiB ceiling is therefore crossed inside
|
|
409
|
+
* the first fraction of the request, and everything after it should
|
|
410
|
+
* never be read. The old code read all of it and then allocated the
|
|
411
|
+
* whole 64 MiB.
|
|
412
|
+
*/
|
|
413
|
+
const bomb: Buffer = gzip(Buffer.alloc(64 * 1024 * 1024));
|
|
414
|
+
const padding: Buffer = Buffer.concat(
|
|
415
|
+
Array.from({ length: 4 }, (): Uint8Array => {
|
|
416
|
+
return oneMiBOfEmptyMembers as unknown as Uint8Array;
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
const compressed: Buffer = Buffer.concat([
|
|
420
|
+
bomb as unknown as Uint8Array,
|
|
421
|
+
padding as unknown as Uint8Array,
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
expect(compressed.length).toBeGreaterThan(4 * 1024 * 1024);
|
|
425
|
+
|
|
426
|
+
const driver: Driver = new Driver();
|
|
427
|
+
|
|
428
|
+
await driver.feed(chunked(compressed, 16 * 1024));
|
|
429
|
+
|
|
430
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
431
|
+
|
|
432
|
+
expect(driver.res.statusCode).toBe(413);
|
|
433
|
+
expect(driver.nextCalls).toHaveLength(0);
|
|
434
|
+
|
|
435
|
+
/*
|
|
436
|
+
* How much INPUT we had to accept before answering is the number that
|
|
437
|
+
* decides whether this is still a denial of service. 512 KiB of a 4 MiB
|
|
438
|
+
* request is a generous bound; the observed figure is far under it.
|
|
439
|
+
*/
|
|
440
|
+
expect(driver.fedBytes).toBeLessThan(512 * 1024);
|
|
441
|
+
|
|
442
|
+
/* It stopped reading rather than draining, and left the socket alone. */
|
|
443
|
+
expect(driver.req.paused).toBe(true);
|
|
444
|
+
expect(driver.req.destroyed).toBe(false);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("51 MiB of EMPTY gzip members is refused even though it inflates to nothing", async () => {
|
|
448
|
+
/*
|
|
449
|
+
* Zero output, so the decompressed cap can never fire. Without the
|
|
450
|
+
* compressed cap this request is unbounded.
|
|
451
|
+
*/
|
|
452
|
+
const driver: Driver = new Driver();
|
|
453
|
+
|
|
454
|
+
await driver.feed(
|
|
455
|
+
Array.from({ length: 52 }, (): Buffer => {
|
|
456
|
+
return oneMiBOfEmptyMembers;
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
461
|
+
|
|
462
|
+
expect(driver.res.statusCode).toBe(413);
|
|
463
|
+
expect(driver.nextCalls).toHaveLength(0);
|
|
464
|
+
expect(
|
|
465
|
+
String((driver.res.jsonBody as { message: string }).message),
|
|
466
|
+
).toContain("compressed");
|
|
467
|
+
expect(driver.req.paused).toBe(true);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("an over-cap Content-Length is refused before a single byte is read", async () => {
|
|
471
|
+
const driver: Driver = new Driver({
|
|
472
|
+
"content-length": String(MAX_COMPRESSED_REQUEST_BODY_BYTES + 1),
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(driver.res.statusCode).toBe(413);
|
|
476
|
+
expect(driver.nextCalls).toHaveLength(0);
|
|
477
|
+
|
|
478
|
+
/*
|
|
479
|
+
* No listeners were attached at all, so the body is never consumed -
|
|
480
|
+
* the cheapest possible refusal.
|
|
481
|
+
*/
|
|
482
|
+
expect(driver.req.listenerCount("data")).toBe(0);
|
|
483
|
+
expect(driver.req.listenerCount("end")).toBe(0);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("both caps are pinned to the identity path's 50 MiB body-parser limit", () => {
|
|
487
|
+
expect(MAX_COMPRESSED_REQUEST_BODY_BYTES).toBe(50 * 1024 * 1024);
|
|
488
|
+
expect(MAX_DECOMPRESSED_REQUEST_BODY_BYTES).toBe(50 * 1024 * 1024);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe("GzipRequestBodyMiddleware - one outcome per request", () => {
|
|
493
|
+
test("data arriving after a 413 does not produce a second response", async () => {
|
|
494
|
+
const driver: Driver = new Driver();
|
|
495
|
+
|
|
496
|
+
await driver.feed(
|
|
497
|
+
Array.from({ length: 52 }, (): Buffer => {
|
|
498
|
+
return oneMiBOfEmptyMembers;
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
503
|
+
expect(driver.res.statusCode).toBe(413);
|
|
504
|
+
|
|
505
|
+
const firstBody: unknown = driver.res.jsonBody;
|
|
506
|
+
|
|
507
|
+
/* Keep shouting at a middleware that has already answered. */
|
|
508
|
+
driver.res.statusCode = 0;
|
|
509
|
+
driver.req.emit("data", oneMiBOfEmptyMembers);
|
|
510
|
+
driver.req.emit("end");
|
|
511
|
+
driver.req.emit("close");
|
|
512
|
+
|
|
513
|
+
await quietPeriod();
|
|
514
|
+
|
|
515
|
+
expect(driver.res.statusCode).toBe(0);
|
|
516
|
+
expect(driver.res.jsonBody).toBe(firstBody);
|
|
517
|
+
expect(driver.nextCalls).toHaveLength(0);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("a response that already has headers is not written to again", () => {
|
|
521
|
+
const req: EventEmitter = new EventEmitter();
|
|
522
|
+
(req as unknown as { headers: Record<string, string> }).headers = {
|
|
523
|
+
"content-length": String(MAX_COMPRESSED_REQUEST_BODY_BYTES + 1),
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const res: FakeResponse = buildResponse();
|
|
527
|
+
|
|
528
|
+
res.headersSent = true;
|
|
529
|
+
|
|
530
|
+
GzipRequestBodyMiddleware.parseBody(
|
|
531
|
+
req as unknown as ExpressRequest,
|
|
532
|
+
res as unknown as ExpressResponse,
|
|
533
|
+
((): void => {}) as NextFunction,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
expect(res.statusCode).toBe(0);
|
|
537
|
+
expect(res.jsonBody).toBeUndefined();
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe("GzipRequestBodyMiddleware - malformed and truncated input", () => {
|
|
542
|
+
test("a body that is not gzip at all is a 500, exactly as before", async () => {
|
|
543
|
+
const driver: Driver = new Driver();
|
|
544
|
+
|
|
545
|
+
await driver.feed([Buffer.from("this is not gzip")]);
|
|
546
|
+
driver.end();
|
|
547
|
+
|
|
548
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
549
|
+
|
|
550
|
+
expect(driver.nextCalls).toHaveLength(0);
|
|
551
|
+
|
|
552
|
+
const error: Exception | undefined = driver.errorResponse();
|
|
553
|
+
|
|
554
|
+
expect(error).toBeDefined();
|
|
555
|
+
expect(error!.code).toBe(ExceptionCode.ServerException);
|
|
556
|
+
expect(error!.message).toBe("Error decompressing data");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("a truncated gzip body is a 500 and never reaches the route", async () => {
|
|
560
|
+
const compressed: Buffer = gzip(Buffer.from("x".repeat(100_000)));
|
|
561
|
+
|
|
562
|
+
const driver: Driver = new Driver();
|
|
563
|
+
|
|
564
|
+
await driver.feed([
|
|
565
|
+
compressed.subarray(0, Math.floor(compressed.length / 2)),
|
|
566
|
+
]);
|
|
567
|
+
driver.end();
|
|
568
|
+
|
|
569
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
570
|
+
|
|
571
|
+
expect(driver.nextCalls).toHaveLength(0);
|
|
572
|
+
expect(driver.req.body).toBeUndefined();
|
|
573
|
+
expect(driver.errorResponse()).toBeDefined();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("an empty body is an empty body, not a 500", async () => {
|
|
577
|
+
/*
|
|
578
|
+
* A bare `Content-Encoding: gzip` header with no body - which any client
|
|
579
|
+
* can put on a GET - used to reach zlib as an empty stream, fail with
|
|
580
|
+
* "unexpected end of file", and cost a 500 plus an error-level log line
|
|
581
|
+
* on every request.
|
|
582
|
+
*/
|
|
583
|
+
const driver: Driver = new Driver();
|
|
584
|
+
|
|
585
|
+
driver.end();
|
|
586
|
+
|
|
587
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
588
|
+
|
|
589
|
+
expect(driver.nextCalls).toEqual([undefined]);
|
|
590
|
+
expect(Buffer.isBuffer(driver.req.body)).toBe(true);
|
|
591
|
+
expect((driver.req.body as Buffer).length).toBe(0);
|
|
592
|
+
expect(driver.errorResponse()).toBeUndefined();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("a request error is handed to Express rather than swallowed", async () => {
|
|
596
|
+
const driver: Driver = new Driver();
|
|
597
|
+
|
|
598
|
+
const failure: Error = new Error("ECONNRESET");
|
|
599
|
+
|
|
600
|
+
await driver.feed([gzip(Buffer.from("partial")).subarray(0, 4)]);
|
|
601
|
+
driver.req.emit("error", failure);
|
|
602
|
+
|
|
603
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
604
|
+
|
|
605
|
+
expect(driver.nextCalls).toEqual([failure]);
|
|
606
|
+
expect(driver.res.statusCode).toBe(0);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("a client that hangs up mid-body gets no response and no next()", async () => {
|
|
610
|
+
const compressed: Buffer = gzip(Buffer.alloc(2 * 1024 * 1024));
|
|
611
|
+
|
|
612
|
+
const driver: Driver = new Driver();
|
|
613
|
+
|
|
614
|
+
await driver.feed(chunked(compressed, 512).slice(0, 2));
|
|
615
|
+
driver.req.emit("close");
|
|
616
|
+
|
|
617
|
+
await quietPeriod();
|
|
618
|
+
|
|
619
|
+
expect(driver.nextCalls).toHaveLength(0);
|
|
620
|
+
expect(driver.res.statusCode).toBe(0);
|
|
621
|
+
expect(driver.req.body).toBeUndefined();
|
|
622
|
+
expect(driver.errorResponse()).toBeUndefined();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("the normal close that follows a completed request is not treated as an abort", async () => {
|
|
626
|
+
const driver: Driver = new Driver();
|
|
627
|
+
|
|
628
|
+
await driver.feed([gzip(Buffer.from("done"))]);
|
|
629
|
+
driver.end();
|
|
630
|
+
/* Node emits this straight after "end" on a completed request. */
|
|
631
|
+
driver.req.emit("close");
|
|
632
|
+
|
|
633
|
+
expect(await driver.waitForOutcome()).toBe(true);
|
|
634
|
+
|
|
635
|
+
expect(driver.nextCalls).toHaveLength(1);
|
|
636
|
+
expect((driver.req.body as Buffer).toString()).toBe("done");
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
describe("GzipRequestBodyMiddleware - rejections do not become a log flood", () => {
|
|
641
|
+
test("a 413 is logged at debug, never at warn or error", () => {
|
|
642
|
+
loggerMock("debug").mockClear();
|
|
643
|
+
loggerMock("warn").mockClear();
|
|
644
|
+
loggerMock("error").mockClear();
|
|
645
|
+
|
|
646
|
+
/* eslint-disable-next-line no-new */
|
|
647
|
+
new Driver({
|
|
648
|
+
"content-length": String(MAX_COMPRESSED_REQUEST_BODY_BYTES + 1),
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
expect(loggerMock("error").mock.calls).toHaveLength(0);
|
|
652
|
+
expect(loggerMock("warn").mock.calls).toHaveLength(0);
|
|
653
|
+
expect(loggerMock("debug").mock.calls).toHaveLength(1);
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
/*
|
|
658
|
+
* The reason the middleware counts output bytes by hand instead of handing
|
|
659
|
+
* zlib a `maxOutputLength`. If a future Node starts honouring the option on
|
|
660
|
+
* streams this test fails and the manual counter can be reconsidered - until
|
|
661
|
+
* then, setting the option would look like a limit while being none.
|
|
662
|
+
*/
|
|
663
|
+
describe("zlib.createGunzip ignores maxOutputLength (why the counter is manual)", () => {
|
|
664
|
+
test("a stream capped at 1 MiB still emits 8 MiB", async () => {
|
|
665
|
+
const compressed: Buffer = gzip(Buffer.alloc(8 * 1024 * 1024));
|
|
666
|
+
|
|
667
|
+
const emitted: number = await new Promise<number>(
|
|
668
|
+
(resolve: (value: number) => void): void => {
|
|
669
|
+
const stream: zlib.Gunzip = zlib.createGunzip({
|
|
670
|
+
maxOutputLength: 1024 * 1024,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
let out: number = 0;
|
|
674
|
+
|
|
675
|
+
stream.on("data", (chunk: Buffer): void => {
|
|
676
|
+
out += chunk.length;
|
|
677
|
+
});
|
|
678
|
+
stream.on("error", (): void => {
|
|
679
|
+
resolve(-1);
|
|
680
|
+
});
|
|
681
|
+
stream.on("end", (): void => {
|
|
682
|
+
resolve(out);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
stream.end(compressed as unknown as Uint8Array);
|
|
686
|
+
},
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
expect(emitted).toBe(8 * 1024 * 1024);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("the same option on the convenience method DOES fire", () => {
|
|
693
|
+
const compressed: Buffer = gzip(Buffer.alloc(8 * 1024 * 1024));
|
|
694
|
+
|
|
695
|
+
expect(() => {
|
|
696
|
+
zlib.gunzipSync(compressed as unknown as Uint8Array, {
|
|
697
|
+
maxOutputLength: 1024 * 1024,
|
|
698
|
+
});
|
|
699
|
+
}).toThrow();
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
/*
|
|
704
|
+
* Source-level assertion, for the same reason the session replay suite has
|
|
705
|
+
* one: a jest test cannot stand up the real Express stack, and this failure
|
|
706
|
+
* mode is silent. If StartServer ever goes back to inflating inline, every
|
|
707
|
+
* limit in this file stops running with no visible symptom.
|
|
708
|
+
*/
|
|
709
|
+
describe("StartServer routes the gzip branch through this middleware", () => {
|
|
710
|
+
const startServerSource: string = fs.readFileSync(
|
|
711
|
+
path.join(__dirname, "../../../Server/Utils/StartServer.ts"),
|
|
712
|
+
"utf-8",
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
test("the gzip branch calls GzipRequestBodyMiddleware.parseBody", () => {
|
|
716
|
+
expect(startServerSource).toContain(
|
|
717
|
+
"GzipRequestBodyMiddleware.parseBody(req, res, next)",
|
|
718
|
+
);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test("StartServer no longer inflates a request body itself", () => {
|
|
722
|
+
expect(startServerSource).not.toContain("zlib.gunzip(");
|
|
723
|
+
expect(startServerSource).not.toMatch(/^import zlib from "zlib";$/m);
|
|
724
|
+
});
|
|
725
|
+
});
|