@oneuptime/common 12.0.6 → 12.0.7
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/AIInsight.ts +1 -1
- package/Models/DatabaseModels/DatabaseBaseModel/FileModel.ts +2 -3
- package/Models/DatabaseModels/DeletedProject.ts +595 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Monitor.ts +111 -0
- package/Models/DatabaseModels/Project.ts +3 -3
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -1
- package/Server/API/FileAPI.ts +15 -2
- package/Server/API/MicrosoftTeamsAPI.ts +6 -1
- package/Server/API/ProjectAPI.ts +214 -0
- package/Server/API/StatusPageAPI.ts +39 -6
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.ts +50 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.ts +67 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/AIAgentService.ts +20 -0
- package/Server/Services/DashboardDomainService.ts +73 -28
- package/Server/Services/DatabaseService.ts +1 -0
- package/Server/Services/DeletedProjectService.ts +138 -0
- package/Server/Services/FileService.ts +61 -0
- package/Server/Services/IncidentEpisodePublicNoteService.ts +18 -0
- package/Server/Services/IncidentPublicNoteService.ts +18 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/MonitorService.ts +311 -0
- package/Server/Services/ProbeService.ts +20 -0
- package/Server/Services/ProjectService.ts +297 -1
- package/Server/Services/PushNotificationService.ts +71 -0
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +18 -0
- package/Server/Services/StatusPageAnnouncementService.ts +53 -0
- package/Server/Services/StatusPageDomainService.ts +73 -28
- package/Server/Services/TeamMemberService.ts +117 -12
- package/Server/Services/TelemetryIngestionKeyService.ts +1 -0
- package/Server/Services/UserWebhookService.ts +34 -109
- package/Server/Services/WorkspaceNotificationRuleService.ts +69 -0
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +37 -1
- package/Server/Types/Database/DeleteById.ts +2 -0
- package/Server/Types/Database/DeleteOneBy.ts +6 -0
- package/Server/Types/Domain.ts +41 -0
- package/Server/Types/Workflow/Components/API/Delete.ts +2 -1
- package/Server/Types/Workflow/Components/API/Get.ts +2 -1
- package/Server/Types/Workflow/Components/API/Patch.ts +2 -1
- package/Server/Types/Workflow/Components/API/Post.ts +2 -1
- package/Server/Types/Workflow/Components/API/Put.ts +2 -1
- package/Server/Types/Workflow/Components/API/Utils.ts +41 -2
- package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +25 -19
- package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +6 -5
- package/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.ts +32 -14
- package/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.ts +31 -13
- package/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.ts +29 -20
- package/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.ts +28 -21
- package/Server/Types/Workflow/Components/BaseModel/LogComponentError.ts +89 -0
- package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +208 -0
- package/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.ts +11 -0
- package/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.ts +41 -18
- package/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.ts +45 -19
- package/Server/Types/Workflow/Components/Discord/SendMessageToChannel.ts +16 -9
- package/Server/Types/Workflow/Components/IncomingWebhookUtils.ts +66 -0
- package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +15 -9
- package/Server/Types/Workflow/Components/Slack/SendMessageToChannel.ts +17 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +1 -1
- package/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.ts +171 -0
- package/Server/Utils/AI/SRE/FixFromIncidentTaskTrigger.ts +3 -3
- package/Server/Utils/AI/SRE/Insights/FixRouting.ts +3 -2
- package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +1 -1
- package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +6 -6
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +43 -5
- package/Server/Utils/DataSource/EgressGuard.ts +176 -16
- package/Server/Utils/DataSource/HttpFetch.ts +6 -45
- package/Server/Utils/Execute.ts +8 -0
- package/Server/Utils/InlineImageAccessTokenSync.ts +23 -0
- package/Server/Utils/LLM/LLMService.ts +57 -12
- package/Server/Utils/Monitor/MonitorAlert.ts +40 -0
- package/Server/Utils/Monitor/MonitorDependencySuppression.ts +192 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +40 -0
- package/Server/Utils/Monitor/MonitorResource.ts +36 -0
- package/Server/Utils/Response.ts +142 -6
- package/Server/Utils/SSRFProtection.ts +273 -60
- package/Server/Utils/StartServer.ts +8 -0
- package/Server/Utils/VM/VMRunner.ts +88 -1
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +555 -49
- package/Server/Utils/Workspace/Slack/Slack.ts +25 -2
- package/Server/Utils/Workspace/WorkspaceBase.ts +6 -0
- package/Tests/App/Dashboard/DangerZone.test.tsx +334 -0
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +539 -0
- package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +678 -0
- package/Tests/App/Dashboard/DashboardVariablesDiscard.test.tsx +706 -0
- package/Tests/App/Dashboard/DashboardVariablesModal.test.tsx +508 -0
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +470 -0
- package/Tests/App/StatusPage/StatusPageDetailPagesNotFound.test.tsx +308 -0
- package/Tests/Models/DatabaseModels/DeletedProjectModel.test.ts +251 -0
- package/Tests/Models/DatabaseModels/SavedViewJSONColumnTuples.test.ts +115 -0
- package/Tests/Server/API/FileAPIAccessControl.test.ts +316 -0
- package/Tests/Server/API/ProjectAPI.test.ts +299 -1
- package/Tests/Server/API/ProjectAPIAdjustBalance.test.ts +459 -0
- package/Tests/Server/API/ProjectAPIAdjustBalanceBillingDisabled.test.ts +177 -0
- package/Tests/Server/Services/DatabaseServiceDeletionReason.test.ts +123 -0
- package/Tests/Server/Services/DeletedProjectService.test.ts +415 -0
- package/Tests/Server/Services/DeletedProjectServiceSelfHosted.test.ts +70 -0
- package/Tests/Server/Services/DomainSubdomainHooks.test.ts +94 -0
- package/Tests/Server/Services/FileIsPublicBooleanMigration.test.ts +244 -0
- package/Tests/Server/Services/FileServiceMimeType.test.ts +89 -0
- package/Tests/Server/Services/MonitorServiceDependencyValidation.test.ts +702 -0
- package/Tests/Server/Services/ProjectServiceAdjustBalance.test.ts +615 -0
- package/Tests/Server/Services/ProjectServiceDeleteAudit.test.ts +406 -0
- package/Tests/Server/Services/PublishTimeImageVisibility.test.ts +450 -0
- package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +669 -0
- package/Tests/Server/Services/UserWebhookSSRF.test.ts +174 -0
- package/Tests/Server/Services/WebPushEndpointAllowlist.test.ts +133 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenRepoint.test.ts +618 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +26 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +215 -0
- package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +742 -0
- package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +256 -0
- package/Tests/Server/Types/Workflow/Components/ModelArguments.test.ts +371 -0
- package/Tests/Server/Utils/AI/CodeAgentCommandGuard.test.ts +204 -0
- package/Tests/Server/Utils/AI/CodeAgentWriteLimits.test.ts +213 -0
- package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +52 -5
- package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +3 -0
- package/Tests/Server/Utils/AI/LLMServiceEgressGuard.test.ts +315 -0
- package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +3 -0
- package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +17 -5
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +14 -1
- package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +35 -0
- package/Tests/Server/Utils/AI/StubLLMEgressGuard.ts +25 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +62 -7
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +69 -0
- package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +330 -0
- package/Tests/Server/Utils/GitHubCreatePullRequestWithToken.test.ts +393 -0
- package/Tests/Server/Utils/InlineImageAccessTokenSync.test.ts +202 -0
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppression.test.ts +811 -0
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +604 -0
- package/Tests/Server/Utils/ResponseSendFileResponse.test.ts +192 -0
- package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +406 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +1169 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +2 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +1605 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTenantResolution.test.ts +1218 -0
- package/Tests/Types/AI/AIChatMessageStatus.test.ts +79 -0
- package/Tests/Types/AI/AIInsightStatus.test.ts +40 -0
- package/Tests/Types/API/HostnameAuthorityParsing.test.ts +157 -0
- package/Tests/Types/Dashboard/DashboardTemplateInvariants.test.ts +27 -0
- package/Tests/Types/DataSource/DataSourceLimits.test.ts +71 -0
- package/Tests/Types/DataSource/DataSourceType.test.ts +181 -0
- package/Tests/Types/DomainSubdomainValidation.test.ts +90 -0
- package/Tests/Types/JSONFunctions.test.ts +252 -1
- package/Tests/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.test.ts +61 -0
- package/Tests/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.test.ts +62 -0
- package/Tests/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.test.ts +68 -0
- package/Tests/Types/Monitor/SnmpMonitor/SnmpVersion.test.ts +51 -0
- package/Tests/UI/Components/404.test.tsx +1 -1
- package/Tests/UI/Components/Alert.test.tsx +1 -1
- package/Tests/UI/Components/Badge.test.tsx +1 -1
- package/Tests/UI/Components/BasicForm.test.tsx +39 -10
- package/Tests/UI/Components/Button.test.tsx +1 -1
- package/Tests/UI/Components/Card.test.tsx +1 -1
- package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +1 -1
- package/Tests/UI/Components/ColorViewer.test.tsx +1 -1
- package/Tests/UI/Components/ComponentsModal.test.tsx +7 -1
- package/Tests/UI/Components/DictionaryOfStrings.test.tsx +1 -1
- package/Tests/UI/Components/Dropdown.test.tsx +1 -1
- package/Tests/UI/Components/FilePicker.test.tsx +1 -1
- package/Tests/UI/Components/HiddenText.test.tsx +1 -1
- package/Tests/UI/Components/Input.test.tsx +1 -1
- package/Tests/UI/Components/Loader.test.tsx +1 -1
- package/Tests/UI/Components/LogTimeRangePicker.test.tsx +1 -1
- package/Tests/UI/Components/MasterPage.test.tsx +1 -1
- package/Tests/UI/Components/ModalPortalDismissal.test.tsx +489 -0
- package/Tests/UI/Components/ModelDelete.test.tsx +214 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableSearchLabels.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableUrlState.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1 -1
- package/Tests/UI/Components/NavBar.test.tsx +1 -1
- package/Tests/UI/Components/OperatorSelectorKeyboard.test.tsx +1 -1
- package/Tests/UI/Components/Pagination.test.tsx +1 -1
- package/Tests/UI/Components/Pill.test.tsx +1 -1
- package/Tests/UI/Components/Probe.test.tsx +1 -1
- package/Tests/UI/Components/ProgressBar.test.tsx +1 -1
- package/Tests/UI/Components/RadioButtons.test.tsx +1 -1
- package/Tests/UI/Components/SideMenuItem.test.tsx +1 -1
- package/Tests/UI/Components/SideOver.test.tsx +500 -6
- package/Tests/UI/Components/Tabs.test.tsx +1 -1
- package/Tests/UI/Components/TelemetrySearchBar.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +1 -1
- package/Tests/UI/Components/TextArea.test.tsx +1 -1
- package/Tests/UI/Components/Toast.test.tsx +1 -1
- package/Tests/UI/Components/Toggle.test.tsx +1 -1
- package/Tests/UI/Components/TopSection.test.tsx +1 -1
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +1 -1
- package/Tests/UI/Rum/ReplayScrubber.test.tsx +1 -1
- package/Tests/UI/Rum/ReplayStage.test.tsx +1 -1
- package/Tests/UI/Telemetry/TelemetrySnapshotWindowAlert.test.tsx +1 -1
- package/Tests/UI/Utils/LocalStorageArrayRoundTrip.test.ts +72 -0
- package/Tests/Utils/Dashboard/HtmlWidgetDocument.test.ts +19 -2
- package/Tests/Utils/Dashboard/ModelQueryVariableInterpolation.test.ts +44 -0
- package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +288 -5
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +42 -0
- package/Tests/Utils/DataSource/DataSourceQueryText.test.ts +27 -0
- package/Tests/Utils/LegacySerializedArray.test.ts +139 -0
- package/Tests/Utils/Monitor/MonitorDependencyRule.test.ts +190 -0
- package/Tests/Utils/Realtime.test.ts +78 -0
- package/Tests/Utils/Telemetry/SavedViewFilters.test.ts +246 -0
- package/Tests/__mocks__/i18next-browser-languagedetector.js +21 -0
- package/Types/AI/FixPullRequestCiStatus.ts +1 -1
- package/Types/AI/FixVerificationStatus.ts +2 -2
- package/Types/API/Hostname.ts +92 -4
- package/Types/API/URL.ts +43 -25
- package/Types/Billing/BalanceAdjustmentType.ts +14 -0
- package/Types/Billing/ProjectBalanceType.ts +13 -0
- package/Types/Domain.ts +26 -0
- package/Types/File/MimeType.ts +11 -0
- package/Types/JSONFunctions.ts +15 -0
- package/Types/Telemetry/TelemetrySavedViewState.ts +9 -1
- package/Types/Workflow/Components/BaseModel.ts +92 -28
- package/Typings/Index.d.ts +12 -0
- package/UI/Components/GanttChart/ChartContainer.tsx +4 -3
- package/UI/Components/Modal/ConfirmModal.tsx +10 -5
- package/UI/Components/Modal/Modal.tsx +27 -47
- package/UI/Components/ModelDelete/ModelDelete.tsx +23 -6
- package/UI/Components/SideOver/SideOver.tsx +195 -68
- package/UI/Utils/PageScrollLock.ts +66 -0
- package/Utils/Dashboard/HtmlWidgetDocument.ts +4 -2
- package/Utils/Dashboard/VariableInterpolation.ts +19 -1
- package/Utils/LegacySerializedArray.ts +60 -0
- package/Utils/Monitor/MonitorDependencyRule.ts +88 -0
- package/Utils/Telemetry/SavedViewFilters.ts +88 -0
- package/build/dist/Models/DatabaseModels/AIInsight.js +1 -1
- package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/FileModel.js +2 -3
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/FileModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DeletedProject.js +640 -0
- package/build/dist/Models/DatabaseModels/DeletedProject.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +106 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +3 -3
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/FileAPI.js +14 -2
- package/build/dist/Server/API/FileAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +6 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProjectAPI.js +157 -4
- package/build/dist/Server/API/ProjectAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +27 -3
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.js +30 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.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/Services/AIAgentService.js +17 -0
- package/build/dist/Server/Services/AIAgentService.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +55 -18
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +1 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DeletedProjectService.js +124 -0
- package/build/dist/Server/Services/DeletedProjectService.js.map +1 -0
- package/build/dist/Server/Services/FileService.js +52 -0
- package/build/dist/Server/Services/FileService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +10 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +10 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +232 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ProbeService.js +17 -0
- package/build/dist/Server/Services/ProbeService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +228 -6
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/PushNotificationService.js +56 -0
- package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +10 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageAnnouncementService.js +54 -0
- package/build/dist/Server/Services/StatusPageAnnouncementService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageDomainService.js +55 -18
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +97 -6
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryIngestionKeyService.js +1 -0
- package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
- package/build/dist/Server/Services/UserWebhookService.js +31 -87
- package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +65 -0
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +23 -0
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Types/Domain.js +37 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Delete.js +2 -1
- package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Get.js +2 -1
- package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Patch.js +2 -1
- package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Post.js +2 -1
- package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Put.js +2 -1
- package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js +35 -2
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +15 -11
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +2 -3
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.js +21 -10
- package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.js +21 -10
- package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.js +18 -13
- package/build/dist/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.js +18 -13
- package/build/dist/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/LogComponentError.js +43 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/LogComponentError.js.map +1 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +116 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js +7 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.js +24 -13
- package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.js +27 -13
- package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Discord/SendMessageToChannel.js +14 -6
- package/build/dist/Server/Types/Workflow/Components/Discord/SendMessageToChannel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/IncomingWebhookUtils.js +42 -0
- package/build/dist/Server/Types/Workflow/Components/IncomingWebhookUtils.js.map +1 -0
- package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +15 -6
- package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Slack/SendMessageToChannel.js +10 -2
- package/build/dist/Server/Types/Workflow/Components/Slack/SendMessageToChannel.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js +141 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +6 -6
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +39 -5
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +94 -10
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/DataSource/HttpFetch.js +1 -19
- package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -1
- package/build/dist/Server/Utils/Execute.js +2 -2
- package/build/dist/Server/Utils/Execute.js.map +1 -1
- package/build/dist/Server/Utils/InlineImageAccessTokenSync.js +13 -0
- package/build/dist/Server/Utils/InlineImageAccessTokenSync.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +43 -12
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +44 -24
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorDependencySuppression.js +158 -0
- package/build/dist/Server/Utils/Monitor/MonitorDependencySuppression.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +50 -30
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +32 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +112 -5
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +219 -53
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +7 -0
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +73 -1
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +445 -43
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +18 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
- package/build/dist/Types/AI/FixPullRequestCiStatus.js +1 -1
- package/build/dist/Types/AI/FixVerificationStatus.js +2 -2
- package/build/dist/Types/API/Hostname.js +73 -4
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/API/URL.js +42 -24
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Billing/BalanceAdjustmentType.js +15 -0
- package/build/dist/Types/Billing/BalanceAdjustmentType.js.map +1 -0
- package/build/dist/Types/Billing/ProjectBalanceType.js +14 -0
- package/build/dist/Types/Billing/ProjectBalanceType.js.map +1 -0
- package/build/dist/Types/Domain.js +22 -0
- package/build/dist/Types/Domain.js.map +1 -1
- package/build/dist/Types/File/MimeType.js +11 -0
- package/build/dist/Types/File/MimeType.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +14 -0
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +83 -28
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/UI/Components/GanttChart/ChartContainer.js +4 -3
- package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +1 -1
- package/build/dist/UI/Components/Modal/ConfirmModal.js +3 -1
- package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +24 -34
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/ModelDelete/ModelDelete.js +11 -6
- package/build/dist/UI/Components/ModelDelete/ModelDelete.js.map +1 -1
- package/build/dist/UI/Components/SideOver/SideOver.js +105 -30
- package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
- package/build/dist/UI/Utils/PageScrollLock.js +52 -0
- package/build/dist/UI/Utils/PageScrollLock.js.map +1 -0
- package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js +4 -2
- package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js.map +1 -1
- package/build/dist/Utils/Dashboard/VariableInterpolation.js +19 -1
- package/build/dist/Utils/Dashboard/VariableInterpolation.js.map +1 -1
- package/build/dist/Utils/LegacySerializedArray.js +51 -0
- package/build/dist/Utils/LegacySerializedArray.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorDependencyRule.js +47 -0
- package/build/dist/Utils/Monitor/MonitorDependencyRule.js.map +1 -0
- package/build/dist/Utils/Telemetry/SavedViewFilters.js +52 -0
- package/build/dist/Utils/Telemetry/SavedViewFilters.js.map +1 -0
- package/jest.config.json +3 -0
- package/package.json +1 -1
- package/tsconfig.json +3 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "@jest/globals";
|
|
2
|
+
import { ExpressRequest, ExpressResponse } from "../../../Server/Utils/Express";
|
|
3
|
+
import FileModel from "../../../Models/DatabaseModels/DatabaseBaseModel/FileModel";
|
|
4
|
+
import MimeType from "../../../Types/File/MimeType";
|
|
5
|
+
import ResponseUtil from "../../../Server/Utils/Response";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* sendFileResponse hands stored bytes back to a browser, and the type it
|
|
9
|
+
* declares them as used to come straight from the upload request. Because
|
|
10
|
+
* /api, /file and /dashboard are one origin, "image/svg+xml" plus a <script>
|
|
11
|
+
* element was enough to run script against dashboard cookies (GHSA-rrqw-5vj9-m9xg).
|
|
12
|
+
*
|
|
13
|
+
* The rule these tests pin down: a browser may render raster images inline and
|
|
14
|
+
* nothing else, it may never sniff, and it may never treat one of these
|
|
15
|
+
* responses as a scriptable document of ours.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface CapturedResponse {
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
statusCode: number | null;
|
|
21
|
+
body: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("Response.sendFileResponse", () => {
|
|
25
|
+
let captured: CapturedResponse;
|
|
26
|
+
let response: ExpressResponse;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
captured = { headers: {}, statusCode: null, body: undefined };
|
|
30
|
+
|
|
31
|
+
response = {
|
|
32
|
+
set: (key: string, value: string): void => {
|
|
33
|
+
captured.headers[key] = value;
|
|
34
|
+
},
|
|
35
|
+
status: (code: number): void => {
|
|
36
|
+
captured.statusCode = code;
|
|
37
|
+
},
|
|
38
|
+
send: (body: unknown): void => {
|
|
39
|
+
captured.body = body;
|
|
40
|
+
},
|
|
41
|
+
} as unknown as ExpressResponse;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
type SendFileFunction = (file: Partial<FileModel>) => Promise<void>;
|
|
45
|
+
|
|
46
|
+
const sendFile: SendFileFunction = async (
|
|
47
|
+
file: Partial<FileModel>,
|
|
48
|
+
): Promise<void> => {
|
|
49
|
+
await ResponseUtil.sendFileResponse(
|
|
50
|
+
{} as ExpressRequest,
|
|
51
|
+
response,
|
|
52
|
+
file as FileModel,
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
test("renders a png inline and still forbids sniffing", async () => {
|
|
57
|
+
await sendFile({
|
|
58
|
+
fileType: MimeType.png,
|
|
59
|
+
file: Buffer.from("png-bytes"),
|
|
60
|
+
name: "logo.png",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(captured.headers["Content-Type"]).toBe("image/png");
|
|
64
|
+
expect(captured.headers["Content-Disposition"]).toContain("inline");
|
|
65
|
+
expect(captured.headers["X-Content-Type-Options"]).toBe("nosniff");
|
|
66
|
+
expect(captured.statusCode).toBe(200);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("still serves image types written before the picker normalised them", async () => {
|
|
70
|
+
/*
|
|
71
|
+
* Until Nov 2025 the file picker stored the browser's MIME string as-is,
|
|
72
|
+
* so upgrading installs hold rows like this. Collapsing them to
|
|
73
|
+
* octet-stream would blank every one of those avatars, because nosniff
|
|
74
|
+
* means an <img> cannot recover from a wrong type.
|
|
75
|
+
*/
|
|
76
|
+
for (const legacyType of [
|
|
77
|
+
"image/vnd.microsoft.icon",
|
|
78
|
+
"image/jpg",
|
|
79
|
+
"image/x-ms-bmp",
|
|
80
|
+
]) {
|
|
81
|
+
await sendFile({
|
|
82
|
+
fileType: legacyType as MimeType,
|
|
83
|
+
file: Buffer.from("bytes"),
|
|
84
|
+
name: "avatar",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(captured.headers["Content-Type"]).toBe(legacyType);
|
|
88
|
+
expect(captured.headers["Content-Disposition"]).toContain("inline");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("serves svg as a download rather than a document", async () => {
|
|
93
|
+
/*
|
|
94
|
+
* The type is preserved on purpose - <img> and <link rel="icon"> ignore
|
|
95
|
+
* Content-Disposition, so status page logos and favicons keep rendering.
|
|
96
|
+
* What changes is a top-level navigation to the URL, which is the only way
|
|
97
|
+
* script inside an SVG ever runs.
|
|
98
|
+
*/
|
|
99
|
+
await sendFile({
|
|
100
|
+
fileType: MimeType.svg,
|
|
101
|
+
file: Buffer.from("<svg><script>alert(1)</script></svg>"),
|
|
102
|
+
name: "logo.svg",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(captured.headers["Content-Type"]).toBe("image/svg+xml");
|
|
106
|
+
expect(captured.headers["Content-Disposition"]).toContain("attachment");
|
|
107
|
+
expect(captured.headers["Content-Disposition"]).toContain(
|
|
108
|
+
'filename="logo.svg"',
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("refuses to echo back a type it does not recognise", async () => {
|
|
113
|
+
// text/html is not in MimeType at all, so it can only be a hand-crafted row.
|
|
114
|
+
await sendFile({
|
|
115
|
+
fileType: "text/html" as MimeType,
|
|
116
|
+
file: Buffer.from("<script>alert(1)</script>"),
|
|
117
|
+
name: "poc",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(captured.headers["Content-Type"]).toBe("application/octet-stream");
|
|
121
|
+
expect(captured.headers["Content-Disposition"]).toContain("attachment");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("degrades junk and empty types to opaque bytes", async () => {
|
|
125
|
+
await sendFile({
|
|
126
|
+
fileType: "not-a-mime-type" as MimeType,
|
|
127
|
+
file: Buffer.from("bytes"),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(captured.headers["Content-Type"]).toBe("application/octet-stream");
|
|
131
|
+
|
|
132
|
+
await sendFile({ file: Buffer.from("bytes") });
|
|
133
|
+
|
|
134
|
+
expect(captured.headers["Content-Type"]).toBe("application/octet-stream");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("neutralises a document-shaped response on every path", async () => {
|
|
138
|
+
for (const fileType of [MimeType.png, MimeType.svg, MimeType.pdf]) {
|
|
139
|
+
await sendFile({ fileType, file: Buffer.from("bytes"), name: "f" });
|
|
140
|
+
|
|
141
|
+
expect(captured.headers["X-Content-Type-Options"]).toBe("nosniff");
|
|
142
|
+
expect(captured.headers["X-Frame-Options"]).toBe("DENY");
|
|
143
|
+
expect(captured.headers["Content-Security-Policy"]).toContain("sandbox");
|
|
144
|
+
expect(captured.headers["Content-Security-Policy"]).toContain(
|
|
145
|
+
"script-src 'none'",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("a crafted filename cannot break out of the header", async () => {
|
|
151
|
+
await sendFile({
|
|
152
|
+
fileType: MimeType.pdf,
|
|
153
|
+
file: Buffer.from("bytes"),
|
|
154
|
+
name: 'evil".pdf\r\nSet-Cookie: a=b',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const disposition: string | undefined =
|
|
158
|
+
captured.headers["Content-Disposition"];
|
|
159
|
+
|
|
160
|
+
expect(disposition).toBeDefined();
|
|
161
|
+
expect(disposition).not.toContain("\r");
|
|
162
|
+
expect(disposition).not.toContain("\n");
|
|
163
|
+
// The only quotes left are the two the header itself uses.
|
|
164
|
+
expect(disposition?.match(/"/g)).toHaveLength(2);
|
|
165
|
+
expect(disposition).toContain('filename="evil_.pdf__Set-Cookie__a_b"');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("keeps a non-ASCII filename instead of reducing it to underscores", async () => {
|
|
169
|
+
await sendFile({
|
|
170
|
+
fileType: MimeType.pdf,
|
|
171
|
+
file: Buffer.from("bytes"),
|
|
172
|
+
name: "月次レポート.pdf",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const disposition: string | undefined =
|
|
176
|
+
captured.headers["Content-Disposition"];
|
|
177
|
+
|
|
178
|
+
// ASCII fallback for old clients, real name for everyone else.
|
|
179
|
+
expect(disposition).toContain('filename="______.pdf"');
|
|
180
|
+
expect(disposition).toContain(
|
|
181
|
+
`filename*=UTF-8''${encodeURIComponent("月次レポート.pdf")}`,
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("falls back to a generic filename when the row has no name", async () => {
|
|
186
|
+
await sendFile({ fileType: MimeType.pdf, file: Buffer.from("bytes") });
|
|
187
|
+
|
|
188
|
+
expect(captured.headers["Content-Disposition"]).toContain(
|
|
189
|
+
'filename="download"',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import SSRFProtection from "../../../Server/Utils/SSRFProtection";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import dns from "dns";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* The blocklist in SSRFProtection used to compare SPELLINGS - "::1", a "fe80:"
|
|
14
|
+
* prefix, a dotted-quad regex - and anything containing a colon skipped DNS
|
|
15
|
+
* resolution entirely. Between those two facts, every one of the addresses in
|
|
16
|
+
* "equivalent spellings" below reached the network: "::ffff:169.254.169.254"
|
|
17
|
+
* is the AWS metadata endpoint written as IPv6, and a dual-stack Linux host
|
|
18
|
+
* connects to it happily.
|
|
19
|
+
*
|
|
20
|
+
* These tests are organised by BYPASS CLASS rather than by URL, because the
|
|
21
|
+
* property that matters is that one address has one verdict no matter how it
|
|
22
|
+
* is written. They cover the guard used by workflow API components
|
|
23
|
+
* (GHSA-v5xh-rw9h-77fv), project webhooks, and status page subscriber
|
|
24
|
+
* webhooks (GHSA-gf3v-98g2-qffx).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
type LookupSpy = jest.SpiedFunction<
|
|
28
|
+
(
|
|
29
|
+
hostname: string,
|
|
30
|
+
options: { all: true },
|
|
31
|
+
) => Promise<Array<{ address: string; family: number }>>
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
let lookupSpy: LookupSpy;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
lookupSpy = jest.spyOn(dns.promises, "lookup") as unknown as LookupSpy;
|
|
38
|
+
lookupSpy.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
jest.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function isBlocked(url: string): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
await SSRFProtection.validateWebhookTargetIsSafe(url);
|
|
48
|
+
return false;
|
|
49
|
+
} catch {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("SSRFProtection — IPv4-mapped and IPv4-embedding IPv6", () => {
|
|
55
|
+
const blocked: Array<[string, string]> = [
|
|
56
|
+
["AWS metadata, IPv4-mapped", "http://[::ffff:169.254.169.254]/latest/"],
|
|
57
|
+
["AWS metadata, mapped in hex", "http://[::ffff:a9fe:a9fe]/latest/"],
|
|
58
|
+
["loopback, IPv4-mapped", "http://[::ffff:127.0.0.1]/"],
|
|
59
|
+
["loopback, mapped in hex", "http://[::ffff:7f00:1]/"],
|
|
60
|
+
["RFC-1918, IPv4-mapped", "http://[::ffff:10.0.0.1]/"],
|
|
61
|
+
["RFC-1918, IPv4-mapped with port", "http://[::ffff:192.168.1.1]:8080/"],
|
|
62
|
+
["CGNAT, IPv4-mapped", "http://[::ffff:100.64.0.1]/"],
|
|
63
|
+
["loopback, IPv4-compatible", "http://[::127.0.0.1]/"],
|
|
64
|
+
["metadata, IPv4-compatible", "http://[::169.254.169.254]/"],
|
|
65
|
+
["metadata via NAT64 prefix", "http://[64:ff9b::169.254.169.254]/"],
|
|
66
|
+
["loopback via NAT64 prefix", "http://[64:ff9b::127.0.0.1]/"],
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
test.each(blocked)("blocks %s", async (_label: string, url: string) => {
|
|
70
|
+
expect(await isBlocked(url)).toBe(true);
|
|
71
|
+
// Literals must be decided without a DNS round trip.
|
|
72
|
+
expect(lookupSpy).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const allowed: Array<[string, string]> = [
|
|
76
|
+
["a public address, IPv4-mapped", "http://[::ffff:8.8.8.8]/"],
|
|
77
|
+
["a public address via NAT64", "http://[64:ff9b::8.8.8.8]/"],
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
test.each(allowed)("allows %s", async (_label: string, url: string) => {
|
|
81
|
+
expect(await isBlocked(url)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("SSRFProtection — equivalent spellings of one address", () => {
|
|
86
|
+
const loopbackSpellings: Array<string> = [
|
|
87
|
+
"http://[::1]/",
|
|
88
|
+
"http://[::1]:8080/",
|
|
89
|
+
"http://[0:0:0:0:0:0:0:1]/",
|
|
90
|
+
"http://[0000:0000:0000:0000:0000:0000:0000:0001]/",
|
|
91
|
+
"http://[0:0:0::1]/",
|
|
92
|
+
"http://[::0001]/",
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
test.each(loopbackSpellings)(
|
|
96
|
+
"blocks IPv6 loopback written as %s",
|
|
97
|
+
async (url: string) => {
|
|
98
|
+
expect(await isBlocked(url)).toBe(true);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const unspecifiedSpellings: Array<string> = [
|
|
103
|
+
"http://[::]/",
|
|
104
|
+
"http://[::0]/",
|
|
105
|
+
"http://[0:0:0:0:0:0:0:0]/",
|
|
106
|
+
"http://[0000:0000:0000:0000:0000:0000:0000:0000]/",
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
test.each(unspecifiedSpellings)(
|
|
110
|
+
"blocks the unspecified address written as %s",
|
|
111
|
+
async (url: string) => {
|
|
112
|
+
expect(await isBlocked(url)).toBe(true);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const linkLocalSpellings: Array<string> = [
|
|
117
|
+
"http://[fe80::1]/",
|
|
118
|
+
"http://[fe80:0:0:0:0:0:0:1]/",
|
|
119
|
+
"http://[FE80::1]/",
|
|
120
|
+
"http://[febf::1]/", // top of the fe80::/10 range
|
|
121
|
+
"http://[fe80::1%25eth0]/", // percent-encoded zone id
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
test.each(linkLocalSpellings)(
|
|
125
|
+
"blocks IPv6 link-local written as %s",
|
|
126
|
+
async (url: string) => {
|
|
127
|
+
expect(await isBlocked(url)).toBe(true);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("SSRFProtection — remaining internal IPv6 ranges", () => {
|
|
133
|
+
const blocked: Array<[string, string]> = [
|
|
134
|
+
["unique-local fc00::/7 (low)", "http://[fc00::1]/"],
|
|
135
|
+
["unique-local fc00::/7 (high)", "http://[fdff::1]/"],
|
|
136
|
+
["unique-local, common ULA", "http://[fd12:3456:789a::1]/"],
|
|
137
|
+
["site-local fec0::/10", "http://[fec0::1]/"],
|
|
138
|
+
["multicast all-nodes", "http://[ff02::1]/"],
|
|
139
|
+
["multicast ff00::/8", "http://[ff00::1]/"],
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
test.each(blocked)("blocks %s", async (_label: string, url: string) => {
|
|
143
|
+
expect(await isBlocked(url)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const allowed: Array<[string, string]> = [
|
|
147
|
+
["Cloudflare public resolver", "http://[2606:4700:4700::1111]/"],
|
|
148
|
+
["Google public resolver", "http://[2001:4860:4860::8888]/"],
|
|
149
|
+
["a public 2000::/3 address with a port", "http://[2606:4700::1]:8443/"],
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
test.each(allowed)(
|
|
153
|
+
"allows public IPv6 %s",
|
|
154
|
+
async (_label: string, url: string) => {
|
|
155
|
+
expect(await isBlocked(url)).toBe(false);
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("SSRFProtection — IPv4 ranges", () => {
|
|
161
|
+
const blocked: Array<string> = [
|
|
162
|
+
"http://0.0.0.0/",
|
|
163
|
+
"http://0.1.2.3/",
|
|
164
|
+
"http://127.0.0.1/",
|
|
165
|
+
"http://127.255.255.254/",
|
|
166
|
+
"http://10.0.0.1/",
|
|
167
|
+
"http://172.16.0.1/",
|
|
168
|
+
"http://172.31.255.254/",
|
|
169
|
+
"http://192.168.0.1/",
|
|
170
|
+
"http://169.254.169.254/latest/meta-data/",
|
|
171
|
+
"http://169.254.169.254:80/latest/meta-data/",
|
|
172
|
+
"http://100.64.0.1/",
|
|
173
|
+
"http://100.127.255.254/",
|
|
174
|
+
"http://224.0.0.1/", // multicast
|
|
175
|
+
"http://255.255.255.255/", // broadcast
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
test.each(blocked)("blocks %s", async (url: string) => {
|
|
179
|
+
expect(await isBlocked(url)).toBe(true);
|
|
180
|
+
expect(lookupSpy).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const allowed: Array<string> = [
|
|
184
|
+
"http://8.8.8.8/",
|
|
185
|
+
"https://1.1.1.1/",
|
|
186
|
+
"http://172.15.255.255/",
|
|
187
|
+
"http://172.32.0.1/",
|
|
188
|
+
"http://100.63.255.255/",
|
|
189
|
+
"http://100.128.0.1/",
|
|
190
|
+
"http://223.255.255.255/",
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
test.each(allowed)("allows %s", async (url: string) => {
|
|
194
|
+
expect(await isBlocked(url)).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("SSRFProtection — alternate IPv4 notations resolve before they are judged", () => {
|
|
199
|
+
/*
|
|
200
|
+
* net.isIP rejects these, so they are not treated as literals - they go to
|
|
201
|
+
* DNS, where getaddrinfo decodes the notation, and the ADDRESS IT RETURNS is
|
|
202
|
+
* what gets checked. That is the property under test: the guard never has to
|
|
203
|
+
* understand decimal or octal IPv4 itself.
|
|
204
|
+
*/
|
|
205
|
+
const notations: Array<[string, string]> = [
|
|
206
|
+
["decimal", "http://2852039166/"],
|
|
207
|
+
["octal", "http://0177.0.0.1/"],
|
|
208
|
+
["hex", "http://0x7f000001/"],
|
|
209
|
+
["short form", "http://127.1/"],
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
test.each(notations)(
|
|
213
|
+
"blocks %s notation once resolved",
|
|
214
|
+
async (_label: string, url: string) => {
|
|
215
|
+
lookupSpy.mockResolvedValue([{ address: "169.254.169.254", family: 4 }]);
|
|
216
|
+
expect(await isBlocked(url)).toBe(true);
|
|
217
|
+
expect(lookupSpy).toHaveBeenCalled();
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("SSRFProtection — DNS answers are judged, not just hostnames", () => {
|
|
223
|
+
test("blocks a public name that resolves to the metadata endpoint", async () => {
|
|
224
|
+
lookupSpy.mockResolvedValue([{ address: "169.254.169.254", family: 4 }]);
|
|
225
|
+
expect(await isBlocked("https://harmless.example.com/hook")).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("blocks a public name that resolves to an internal IPv6 address", async () => {
|
|
229
|
+
lookupSpy.mockResolvedValue([{ address: "::1", family: 6 }]);
|
|
230
|
+
expect(await isBlocked("https://harmless.example.com/hook")).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("blocks a public name that resolves to an IPv4-mapped internal address", async () => {
|
|
234
|
+
lookupSpy.mockResolvedValue([
|
|
235
|
+
{ address: "::ffff:169.254.169.254", family: 6 },
|
|
236
|
+
]);
|
|
237
|
+
expect(await isBlocked("https://harmless.example.com/hook")).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("blocks when only ONE of several answers is internal", async () => {
|
|
241
|
+
lookupSpy.mockResolvedValue([
|
|
242
|
+
{ address: "93.184.216.34", family: 4 },
|
|
243
|
+
{ address: "8.8.8.8", family: 4 },
|
|
244
|
+
{ address: "10.1.2.3", family: 4 },
|
|
245
|
+
]);
|
|
246
|
+
expect(await isBlocked("https://round-robin.example.com/hook")).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("blocks when DNS resolution fails outright", async () => {
|
|
250
|
+
lookupSpy.mockRejectedValue(new Error("ENOTFOUND"));
|
|
251
|
+
expect(await isBlocked("https://nope.invalid/hook")).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("allows a name whose every answer is public", async () => {
|
|
255
|
+
lookupSpy.mockResolvedValue([
|
|
256
|
+
{ address: "93.184.216.34", family: 4 },
|
|
257
|
+
{ address: "2606:4700:4700::1111", family: 6 },
|
|
258
|
+
]);
|
|
259
|
+
expect(await isBlocked("https://hooks.example.com/hook")).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("SSRFProtection — hostname forms of internal targets", () => {
|
|
264
|
+
const blocked: Array<string> = [
|
|
265
|
+
"http://localhost/",
|
|
266
|
+
"http://localhost:3000/",
|
|
267
|
+
"http://localhost./",
|
|
268
|
+
"http://api.localhost/",
|
|
269
|
+
"http://metadata.google.internal/computeMetadata/v1/",
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
test.each(blocked)("blocks %s", async (url: string) => {
|
|
273
|
+
expect(await isBlocked(url)).toBe(true);
|
|
274
|
+
expect(lookupSpy).not.toHaveBeenCalled();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("SSRFProtection — scheme handling", () => {
|
|
279
|
+
/*
|
|
280
|
+
* URL.fromString silently defaults an unrecognized scheme to https, so the
|
|
281
|
+
* scheme has to be read off the raw string or "file:///etc/passwd" arrives
|
|
282
|
+
* at the blocklist already rewritten as the host "file".
|
|
283
|
+
*/
|
|
284
|
+
const blockedSchemes: Array<string> = [
|
|
285
|
+
"file:///etc/passwd",
|
|
286
|
+
"gopher://127.0.0.1:6379/_SET%20x%20y",
|
|
287
|
+
"ftp://internal.example.com/",
|
|
288
|
+
"dict://127.0.0.1:11211/stat",
|
|
289
|
+
"ldap://127.0.0.1:389/",
|
|
290
|
+
"jar://127.0.0.1/",
|
|
291
|
+
"mongodb://8.8.8.8/",
|
|
292
|
+
"ws://8.8.8.8/",
|
|
293
|
+
"wss://8.8.8.8/",
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
test.each(blockedSchemes)("rejects %s", async (url: string) => {
|
|
297
|
+
expect(await isBlocked(url)).toBe(true);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("rejects a URL with no host", async () => {
|
|
301
|
+
expect(await isBlocked("http:///no-host")).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("rejects a protocol-relative URL", async () => {
|
|
305
|
+
expect(await isBlocked("//evil.example.com/hook")).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("still accepts a scheme-less host:port, which is not a scheme", async () => {
|
|
309
|
+
expect(await isBlocked("hooks.example.com:8443/webhook")).toBe(false);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test.each(["http", "https", "HTTP", "HTTPS"])(
|
|
313
|
+
"accepts the %s scheme",
|
|
314
|
+
async (scheme: string) => {
|
|
315
|
+
expect(await isBlocked(`${scheme}://hooks.example.com/webhook`)).toBe(
|
|
316
|
+
false,
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("SSRFProtection — host extraction helpers", () => {
|
|
323
|
+
test("getBareHostname strips scheme, port, path and query", () => {
|
|
324
|
+
expect(
|
|
325
|
+
SSRFProtection.getBareHostname(
|
|
326
|
+
"https://hooks.slack.com:443/services/x?a=b",
|
|
327
|
+
),
|
|
328
|
+
).toBe("hooks.slack.com");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("getBareHostname unwraps a bracketed IPv6 literal", () => {
|
|
332
|
+
expect(SSRFProtection.getBareHostname("http://[::1]:8080/x")).toBe("::1");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
/*
|
|
336
|
+
* The allowlist has to parse the host the way axios will. OneUptime's own
|
|
337
|
+
* URL.fromString takes everything before the first "/" as the authority and
|
|
338
|
+
* never stops at "?" or "#", so it read the host of
|
|
339
|
+
* "https://169.254.169.254#.office.com" as "169.254.169.254#.office.com" -
|
|
340
|
+
* which ends with ".office.com" and passed the Teams pin, while the request
|
|
341
|
+
* went to 169.254.169.254. Reachable by unauthenticated status page
|
|
342
|
+
* subscribers.
|
|
343
|
+
*/
|
|
344
|
+
const parserDifferentials: Array<[string, string]> = [
|
|
345
|
+
["fragment", "https://169.254.169.254#.office.com"],
|
|
346
|
+
["query", "https://169.254.169.254?.office.com"],
|
|
347
|
+
["fragment after a public host", "https://evil.example.com#.office.com"],
|
|
348
|
+
["query after a public host", "https://evil.example.com?.office.com"],
|
|
349
|
+
["fragment with a path", "https://evil.example.com#.office.com/webhook"],
|
|
350
|
+
["userinfo", "https://outlook.office.com@evil.example.com/webhook"],
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
test.each(parserDifferentials)(
|
|
354
|
+
"isUrlOnAllowedDomain is not fooled by a %s",
|
|
355
|
+
(_label: string, url: string) => {
|
|
356
|
+
expect(SSRFProtection.isUrlOnAllowedDomain(url, ["office.com"])).toBe(
|
|
357
|
+
false,
|
|
358
|
+
);
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
test.each(parserDifferentials)(
|
|
363
|
+
"getBareHostname agrees with the WHATWG parser on a %s",
|
|
364
|
+
(_label: string, url: string) => {
|
|
365
|
+
expect(SSRFProtection.getBareHostname(url)).toBe(
|
|
366
|
+
new globalThis.URL(url).hostname.toLowerCase(),
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
test("isUrlOnAllowedDomain pins on the host, not on a substring of the URL", () => {
|
|
372
|
+
// The classic break: an attacker-controlled path that contains the domain.
|
|
373
|
+
expect(
|
|
374
|
+
SSRFProtection.isUrlOnAllowedDomain(
|
|
375
|
+
"https://169.254.169.254/?x=hooks.slack.com",
|
|
376
|
+
["hooks.slack.com"],
|
|
377
|
+
),
|
|
378
|
+
).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("isUrlOnAllowedDomain anchors suffix matching on a dot", () => {
|
|
382
|
+
expect(
|
|
383
|
+
SSRFProtection.isUrlOnAllowedDomain("https://evil-slack.com/x", [
|
|
384
|
+
"slack.com",
|
|
385
|
+
]),
|
|
386
|
+
).toBe(false);
|
|
387
|
+
expect(
|
|
388
|
+
SSRFProtection.isUrlOnAllowedDomain("https://slack.com.evil.tld/x", [
|
|
389
|
+
"slack.com",
|
|
390
|
+
]),
|
|
391
|
+
).toBe(false);
|
|
392
|
+
expect(
|
|
393
|
+
SSRFProtection.isUrlOnAllowedDomain("https://hooks.slack.com/x", [
|
|
394
|
+
"slack.com",
|
|
395
|
+
]),
|
|
396
|
+
).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("isUrlOnAllowedDomain requires https", () => {
|
|
400
|
+
expect(
|
|
401
|
+
SSRFProtection.isUrlOnAllowedDomain("http://hooks.slack.com/x", [
|
|
402
|
+
"slack.com",
|
|
403
|
+
]),
|
|
404
|
+
).toBe(false);
|
|
405
|
+
});
|
|
406
|
+
});
|