@oneuptime/common 14.0.1 → 14.0.2
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/AlertEpisodeMember.ts +8 -1
- package/Models/DatabaseModels/AutoRemediationSuggestion.ts +63 -0
- package/Models/DatabaseModels/IncidentEpisodeMember.ts +8 -1
- package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +9 -2
- package/Models/DatabaseModels/IncidentPublicNote.ts +9 -2
- package/Models/DatabaseModels/KubernetesCluster.ts +429 -0
- package/Models/DatabaseModels/Monitor.ts +48 -0
- package/Models/DatabaseModels/Runner.ts +5 -4
- package/Models/DatabaseModels/RunnerJob.ts +66 -0
- package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +10 -2
- package/Models/DatabaseModels/WorkflowVariable.ts +479 -2
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +79 -25
- package/Server/API/AIInvestigationAPI.ts +341 -0
- package/Server/API/AutoRemediationAPI.ts +184 -2
- package/Server/API/CommonAPI.ts +70 -0
- package/Server/API/KubernetesClusterAiAccessAPI.ts +793 -0
- package/Server/API/MicrosoftTeamsAPI.ts +770 -315
- package/Server/API/MonitorAPI.ts +140 -0
- package/Server/API/OnCallScheduleTimelineAPI.ts +5 -21
- package/Server/API/SlackAPI.ts +447 -99
- package/Server/API/StatusPageAPI.ts +146 -26
- package/Server/API/TelemetryAPI.ts +216 -5
- package/Server/API/UserTwoFactorBackupCodeAPI.ts +1 -0
- package/Server/API/WorkflowVariableAPI.ts +156 -0
- package/Server/Infrastructure/GlobalCache.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.ts +97 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.ts +129 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.ts +20 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +28 -0
- package/Server/Middleware/TelemetryIngest.ts +47 -2
- package/Server/Middleware/UserAuthorization.ts +56 -2
- package/Server/Services/AIAgentService.ts +1 -0
- package/Server/Services/AccessTokenService.ts +121 -48
- package/Server/Services/AlertEpisodeMemberService.ts +9 -0
- package/Server/Services/AlertEpisodeService.ts +14 -1
- package/Server/Services/AlertService.ts +134 -22
- package/Server/Services/AutoRemediationRuleEngineService.ts +968 -39
- package/Server/Services/AutoRemediationRuleService.ts +118 -0
- package/Server/Services/BillingService.ts +1 -0
- package/Server/Services/InboundEmail/Providers/SendGridInboundProvider.ts +16 -22
- package/Server/Services/IncidentEpisodeMemberService.ts +9 -0
- package/Server/Services/IncidentEpisodePublicNoteService.ts +66 -0
- package/Server/Services/IncidentEpisodeRoleMemberService.ts +27 -0
- package/Server/Services/IncidentEpisodeService.ts +14 -1
- package/Server/Services/IncidentGroupingEngineService.ts +11 -0
- package/Server/Services/IncidentMemberService.ts +21 -0
- package/Server/Services/IncidentPublicNoteService.ts +63 -0
- package/Server/Services/IncidentService.ts +120 -20
- package/Server/Services/IncidentStateTimelineService.ts +15 -0
- package/Server/Services/IncidentTemplateService.ts +83 -3
- package/Server/Services/KubernetesClusterAiAccessService.ts +1943 -0
- package/Server/Services/KubernetesClusterService.ts +1570 -1
- package/Server/Services/LogAggregationService.ts +14 -6
- package/Server/Services/MonitorService.ts +651 -52
- package/Server/Services/MonitorStatusTimelineService.ts +639 -2
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -0
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +3 -0
- package/Server/Services/OnCallNotificationAlertingService.ts +36 -12
- package/Server/Services/OnCallSetupReminderService.ts +1 -0
- package/Server/Services/ProbeService.ts +7 -1
- package/Server/Services/ProjectService.ts +11 -1
- package/Server/Services/RunbookCredentialService.ts +26 -1
- package/Server/Services/RunbookSecretService.ts +36 -0
- package/Server/Services/RunnerJobService.ts +941 -23
- package/Server/Services/RunnerService.ts +439 -6
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +66 -0
- package/Server/Services/ScheduledMaintenanceService.ts +124 -19
- package/Server/Services/ScheduledMaintenanceTemplateService.ts +82 -3
- package/Server/Services/ServiceLevelObjectiveService.ts +14 -1
- package/Server/Services/StatusPagePrivateUserService.ts +1 -0
- package/Server/Services/StatusPageService.ts +301 -74
- package/Server/Services/StatusPageSubscriberService.ts +4 -0
- package/Server/Services/TeamMemberService.ts +309 -7
- package/Server/Services/TraceAggregationService.ts +10 -6
- package/Server/Services/UserEmailService.ts +1 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +1 -0
- package/Server/Services/UserNotificationRuleAdminService.ts +1 -0
- package/Server/Services/UserNotificationRuleService.ts +4 -0
- package/Server/Services/UserService.ts +122 -1
- package/Server/Services/WorkflowVariableService.ts +593 -8
- package/Server/Services/WorkspaceNotificationRuleService.ts +588 -8
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +170 -1
- package/Server/Types/Database/QueryHelper.ts +22 -0
- package/Server/Types/Markdown.ts +502 -27
- package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +182 -0
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +525 -0
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +854 -0
- package/Server/Utils/AI/CodeFix/CodeFixAgentAuth.ts +22 -2
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1916 -307
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +1601 -74
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +139 -7
- package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +58 -1
- package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +54 -1
- package/Server/Utils/AI/Toolbox/AIActionTools.ts +14 -6
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +4 -2
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +1341 -150
- package/Server/Utils/AutoRemediation/RemediationVerifier.ts +375 -4
- package/Server/Utils/Database/AffectedResourceRelations.ts +127 -0
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +240 -1
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +1 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +83 -9
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +5 -1
- package/Server/Utils/Monitor/MonitorIncident.ts +104 -9
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +124 -7
- package/Server/Utils/Monitor/ProbeMonitorsNotification.ts +407 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +1 -0
- package/Server/Utils/PushNotificationUtil.ts +45 -0
- package/Server/Utils/Rules/OwnerRuleAssignment.ts +40 -3
- package/Server/Utils/Runbook/RunbookExecutePermission.ts +56 -6
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +223 -33
- package/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.ts +559 -0
- package/Server/Utils/StartServer.ts +4 -3
- package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +528 -0
- package/Server/Utils/TwoFactorBackupCodeNotification.ts +1 -0
- package/Server/Utils/UserPermission/UserPermission.ts +16 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +704 -0
- package/Server/Utils/Workflow/WorkflowVariableOAuthToken.ts +318 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +47 -7
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +57 -16
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +193 -214
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +57 -13
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.ts +54 -17
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +179 -161
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +281 -167
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.ts +114 -0
- package/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.ts +895 -0
- package/Server/Utils/Workspace/Slack/Actions/ActionTypes.ts +7 -9
- package/Server/Utils/Workspace/Slack/Actions/Alert.ts +96 -227
- package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +93 -214
- package/Server/Utils/Workspace/Slack/Actions/Auth.ts +30 -3
- package/Server/Utils/Workspace/Slack/Actions/Authorization.ts +99 -0
- package/Server/Utils/Workspace/Slack/Actions/Incident.ts +117 -283
- package/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.ts +101 -196
- package/Server/Utils/Workspace/Slack/Actions/ReactionNote.ts +329 -0
- package/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.ts +111 -287
- package/Server/Utils/Workspace/Slack/Slack.ts +86 -17
- package/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
- package/Server/Utils/Workspace/Workspace.ts +171 -54
- package/Server/Utils/Workspace/WorkspaceActionAuthorization.ts +252 -0
- package/Server/Utils/Workspace/WorkspaceBase.ts +39 -0
- package/Server/Utils/Workspace/WorkspaceOAuthState.ts +280 -0
- package/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.ts +96 -0
- package/Server/Utils/Workspace/WorkspaceReactionNote.ts +620 -0
- package/Tests/App/Dashboard/AttributesJSONDashboardSurfaces.test.tsx +496 -0
- package/Tests/App/Dashboard/AutoRemediationRulesTableCommandRunners.test.tsx +152 -0
- package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +32 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +794 -0
- package/Tests/App/Dashboard/CreateOAuthWorkflowVariableModal.test.tsx +840 -0
- package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +653 -0
- package/Tests/App/Dashboard/DatabaseMonitorStepForm.test.tsx +197 -0
- package/Tests/App/Dashboard/DatabaseMonitorView.test.tsx +376 -0
- package/Tests/App/Dashboard/DisabledWarningRobustness.test.tsx +262 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +2080 -0
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +486 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +933 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +218 -0
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +665 -0
- package/Tests/App/Dashboard/IncidentEpisodeFeedPublicNoteNotifyDefault.test.tsx +494 -0
- package/Tests/App/Dashboard/IncidentEpisodeOverviewNotifyDefault.test.tsx +706 -0
- package/Tests/App/Dashboard/IncidentFeedPublicNoteNotifyDefault.test.tsx +472 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +858 -0
- package/Tests/App/Dashboard/IncomingEmailAddressSettings.test.tsx +567 -0
- package/Tests/App/Dashboard/InvestigationClusterToolFormat.test.ts +241 -0
- package/Tests/App/Dashboard/InvestigationEvidenceKubectl.test.tsx +322 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +12 -0
- package/Tests/App/Dashboard/InvestigationPanelClusterAccess.test.tsx +476 -0
- package/Tests/App/Dashboard/InvestigationRunDetailsKubectl.test.tsx +383 -0
- package/Tests/App/Dashboard/KubectlActivitySummary.test.tsx +366 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +2886 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +2688 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +915 -0
- package/Tests/App/Dashboard/LayerDateTimeFieldTimezoneName.test.tsx +66 -0
- package/Tests/App/Dashboard/LogMonitorPreview.test.tsx +174 -0
- package/Tests/App/Dashboard/MonitorFeedRefresh.test.tsx +279 -0
- package/Tests/App/Dashboard/MonitorMetricQueryConfig.test.ts +244 -0
- package/Tests/App/Dashboard/MonitorOpenWorkCard.test.tsx +381 -0
- package/Tests/App/Dashboard/MonitorOpenWorkHook.test.tsx +784 -0
- package/Tests/App/Dashboard/MonitorOverviewData.test.tsx +2199 -0
- package/Tests/App/Dashboard/MonitorOverviewHero.test.tsx +1169 -0
- package/Tests/App/Dashboard/MonitorOverviewInput.test.ts +1127 -0
- package/Tests/App/Dashboard/MonitorOverviewPage.test.tsx +2136 -0
- package/Tests/App/Dashboard/MonitorOverviewSelectReadable.test.ts +368 -0
- package/Tests/App/Dashboard/MonitorOverviewStatBar.test.tsx +526 -0
- package/Tests/App/Dashboard/MonitorOverviewSupportingCards.test.tsx +542 -0
- package/Tests/App/Dashboard/MonitorOwnersCombine.test.ts +299 -0
- package/Tests/App/Dashboard/MonitorProbesCard.test.tsx +602 -0
- package/Tests/App/Dashboard/MonitorResponseTimeCard.test.tsx +386 -0
- package/Tests/App/Dashboard/MonitorSetupAndConnectionCards.test.tsx +612 -0
- package/Tests/App/Dashboard/MonitorStatusChangesCard.test.tsx +257 -0
- package/Tests/App/Dashboard/MonitorSummaryTestMonitorButton.test.tsx +360 -1
- package/Tests/App/Dashboard/MonitorUptimeHistoryCard.test.tsx +835 -0
- package/Tests/App/Dashboard/MonitorUptimeSummaryHook.test.tsx +971 -0
- package/Tests/App/Dashboard/MonitorViewLayout.test.tsx +580 -0
- package/Tests/App/Dashboard/OnCallLayerRestrictionTimesPreserved.test.tsx +441 -0
- package/Tests/App/Dashboard/OnCallNavbar.test.tsx +214 -0
- package/Tests/App/Dashboard/OnCallSchedulePreviewCalendarDisplay.test.tsx +1389 -0
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +205 -1
- package/Tests/App/Dashboard/OnCallScheduleTimeInputsSettingsZone.test.tsx +198 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +524 -0
- package/Tests/App/Dashboard/RemediationSuggestionCardApproval.test.tsx +702 -0
- package/Tests/App/Dashboard/RemoveUserFromProject.test.tsx +427 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +4 -0
- package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +3 -1
- package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +166 -0
- package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +622 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceChangeStateNotifyDefault.test.tsx +1491 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedPublicNoteNotifyDefault.test.tsx +489 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewNotifyDefault.test.tsx +909 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +14 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceWindowStatsTranslation.test.tsx +307 -0
- package/Tests/App/Dashboard/SecurityEventAttributeColumnPicker.test.tsx +542 -0
- package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +174 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsEmptyState.test.tsx +973 -212
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +491 -52
- package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +301 -0
- package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +158 -1
- package/Tests/App/Dashboard/SecurityEventProviderTile.test.tsx +869 -0
- package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +959 -94
- package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +2 -0
- package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +274 -1
- package/Tests/App/Dashboard/TeamMembershipRemoval.test.ts +900 -0
- package/Tests/App/Dashboard/TeamMembershipRemovalPages.test.tsx +460 -0
- package/Tests/App/Dashboard/TimezoneElements.test.tsx +321 -0
- package/Tests/App/Dashboard/TimezoneSelectButton.test.tsx +634 -0
- package/Tests/App/Dashboard/TraceExplorer.test.tsx +260 -0
- package/Tests/App/Dashboard/TraceMonitorPreview.test.tsx +182 -0
- package/Tests/App/Dashboard/UserTimezoneInit.test.tsx +544 -0
- package/Tests/App/Dashboard/WorkflowVariableModals.test.tsx +1630 -0
- package/Tests/App/Dashboard/WorkflowVariableRoutes.test.tsx +603 -0
- package/Tests/App/Dashboard/WorkflowVariableTokenStatus.test.tsx +589 -0
- package/Tests/App/Dashboard/WorkflowVariableUtil.test.ts +1543 -0
- package/Tests/App/Dashboard/WorkflowVariableView.test.tsx +3227 -0
- package/Tests/App/Dashboard/WorkflowVariablesTable.test.tsx +1277 -395
- package/Tests/App/StatusPage/StatusPageOverviewMonitorGroupUptime.test.tsx +714 -0
- package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.NewYork.test.tsx +1009 -0
- package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.test.tsx +1092 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +5 -0
- package/Tests/Models/DatabaseModels/KubernetesClusterAiAccessColumns.test.ts +520 -0
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +66 -2
- package/Tests/Models/DatabaseModels/WorkspaceProjectAuthTokenAccessControl.test.ts +236 -0
- package/Tests/Models/IncidentEpisodePublicNoteNotifyColumnContract.test.ts +103 -0
- package/Tests/Models/IncidentPublicNoteNotifyColumnContract.test.ts +96 -0
- package/Tests/Models/ScheduledMaintenancePublicNoteNotifyColumnContract.test.ts +111 -0
- package/Tests/Server/API/AIInvestigationAPI.test.ts +976 -0
- package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +836 -0
- package/Tests/Server/API/CommonAPIAssertCanCreateTable.test.ts +565 -0
- package/Tests/Server/API/CommonAPIAssertCanReadTable.test.ts +282 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +1744 -0
- package/Tests/Server/API/LogHistogramAPI.test.ts +371 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +125 -30
- package/Tests/Server/API/MicrosoftTeamsOAuthState.test.ts +1121 -0
- package/Tests/Server/API/MonitorUptimeSummaryAPI.test.ts +685 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +602 -1
- package/Tests/Server/API/SlackChannelCacheAPI.test.ts +358 -0
- package/Tests/Server/API/SlackOAuthState.test.ts +688 -0
- package/Tests/Server/API/StatusPageOverviewMonitorGroupUptime.test.ts +527 -0
- package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +2 -0
- package/Tests/Server/API/StatusPageUptimeAPI.test.ts +427 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +4 -0
- package/Tests/Server/API/WorkflowVariableAPI.test.ts +404 -0
- package/Tests/Server/API/WorkspaceOAuthTestHelpers.ts +289 -0
- package/Tests/Server/API/WorkspaceSendTestNotificationRoutes.test.ts +2064 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +86 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOAuth2WorkflowVariablesMigration.test.ts +254 -0
- package/Tests/Server/Middleware/BlockedUserMiddleware.test.ts +403 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +13 -0
- package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +2 -0
- package/Tests/Server/Middleware/RemovedProjectMemberAuthorization.test.ts +469 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +77 -1
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +211 -0
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +358 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +97 -0
- package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +2 -0
- package/Tests/Server/Services/AccessTokenProjectUserGrant.test.ts +14 -2
- package/Tests/Server/Services/AccessTokenRemovedMemberAccess.test.ts +1044 -0
- package/Tests/Server/Services/AddOwnersIdempotency.test.ts +5 -0
- package/Tests/Server/Services/AlertEpisodeMemberCreatePermission.test.ts +139 -0
- package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +1361 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +375 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +2384 -0
- package/Tests/Server/Services/FindOwnersProjectMembership.test.ts +267 -0
- package/Tests/Server/Services/InboundEmail/SendGridInboundProviderAddress.test.ts +90 -0
- package/Tests/Server/Services/IncidentEpisodeMemberCreatePermission.test.ts +143 -0
- package/Tests/Server/Services/IncidentEpisodePublicNoteNotifyDefault.test.ts +805 -0
- package/Tests/Server/Services/IncidentPublicNoteNotifyDefault.test.ts +724 -0
- package/Tests/Server/Services/IncidentRoleProjectMembership.test.ts +233 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessAgentRunnerAssignments.test.ts +372 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessBindingGuard.test.ts +1876 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +620 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +421 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +3662 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessSubjectClusters.test.ts +405 -0
- package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +975 -0
- package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +497 -0
- package/Tests/Server/Services/KubernetesClusterServiceDeleteSettlesRemediation.test.ts +328 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +99 -0
- package/Tests/Server/Services/MonitorServiceIncomingEmailCustomAddress.test.ts +447 -0
- package/Tests/Server/Services/MonitorServiceProbeFlagSync.test.ts +1334 -0
- package/Tests/Server/Services/MonitorServiceProbeStatusGrouping.test.ts +1640 -0
- package/Tests/Server/Services/MonitorServiceRefreshCurrentStatus.test.ts +225 -0
- package/Tests/Server/Services/MonitorStatusBridgeProbePath.test.ts +8 -8
- package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +264 -1
- package/Tests/Server/Services/MonitorStatusTimelineMergedDowntime.test.ts +284 -0
- package/Tests/Server/Services/MonitorUptimeSummaryService.test.ts +1043 -0
- package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +5 -0
- package/Tests/Server/Services/OnCallUndeliverablePageOwnerMembership.test.ts +359 -0
- package/Tests/Server/Services/ProbeServiceDashboardLink.test.ts +191 -0
- package/Tests/Server/Services/ProjectOwnersProjectMembership.test.ts +221 -0
- package/Tests/Server/Services/RuleRun/IncidentAlertMaintenanceRuleEnginesRun.test.ts +5 -0
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupARun.test.ts +28 -0
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +9 -1
- package/Tests/Server/Services/RunnerJobEnqueueAiCommandAgentRunner.test.ts +230 -0
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +978 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +866 -0
- package/Tests/Server/Services/RunnerJobSubmitResultRedaction.test.ts +322 -0
- package/Tests/Server/Services/RunnerJobSubmitResultUnstorableText.test.ts +357 -0
- package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +227 -0
- package/Tests/Server/Services/RunnerServiceAgentName.test.ts +581 -0
- package/Tests/Server/Services/ScheduledMaintenancePublicNoteNotifyDefault.test.ts +802 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +53 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +5 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +12 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +1176 -1
- package/Tests/Server/Services/TeamMemberBillingSeatSync.test.ts +9 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +18 -0
- package/Tests/Server/Services/TeamMemberProjectMembership.test.ts +255 -0
- package/Tests/Server/Services/TeamMemberRemovalRevokesProjectAccess.test.ts +600 -0
- package/Tests/Server/Services/TeamMemberResourceCleanup.test.ts +731 -0
- package/Tests/Server/Services/TeamMemberWorkspaceAccountCleanup.test.ts +164 -0
- package/Tests/Server/Services/TraceAggregationService.test.ts +64 -0
- package/Tests/Server/Services/UserServiceBlockedUser.test.ts +293 -0
- package/Tests/Server/Services/WorkflowVariableOAuthHooks.test.ts +1016 -0
- package/Tests/Server/Services/WorkflowVariableOAuthPostgres.test.ts +543 -0
- package/Tests/Server/Services/WorkflowVariableRenameUniqueness.test.ts +16 -5
- package/Tests/Server/Services/WorkspaceNotificationRuleNoteDelivery.test.ts +473 -0
- package/Tests/Server/Services/WorkspaceNotificationRuleSendTestNotification.test.ts +3690 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenMiscDataSecrets.test.ts +374 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenSlackChannelCache.test.ts +143 -0
- package/Tests/Server/Types/Database/Permissions/WorkflowVariableOAuthColumnPermission.test.ts +250 -0
- package/Tests/Server/Types/Database/QueryHelperJsonContains.test.ts +96 -0
- package/Tests/Server/Types/Markdown.test.ts +1033 -0
- package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +285 -0
- package/Tests/Server/Utils/AI/CodeFix/CodeFixAgentAuth.test.ts +159 -1
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +752 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +1840 -0
- package/Tests/Server/Utils/AI/KubectlJobRunner.test.ts +1067 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +355 -0
- package/Tests/Server/Utils/AI/PostIncidentStatusUpdateTool.test.ts +496 -0
- package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +305 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +1365 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +639 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +1585 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +1065 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +2138 -1
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +1035 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +517 -0
- package/Tests/Server/Utils/AI/ResourceTools.test.ts +27 -1
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +1371 -24
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorRollbackRace.test.ts +962 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +518 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +939 -2
- package/Tests/Server/Utils/Database/ProjectScopedRelationReferences.test.ts +371 -0
- package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +64 -0
- package/Tests/Server/Utils/Monitor/DatabaseMetricObservationRemediation.test.ts +102 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +362 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +55 -0
- package/Tests/Server/Utils/Monitor/MonitorEmailAddressRedaction.test.ts +255 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +347 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +203 -0
- package/Tests/Server/Utils/Monitor/ProbeMonitorsNotification.test.ts +1147 -0
- package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +79 -0
- package/Tests/Server/Utils/ResourceFeed/ResourceFeedUtil.test.ts +21 -1
- package/Tests/Server/Utils/Rules/OwnerRuleAssignment.test.ts +155 -1
- package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +288 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.test.ts +304 -0
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +40 -3
- package/Tests/Server/Utils/Workflow/OAuth2TokenClient.test.ts +702 -0
- package/Tests/Server/Utils/Workflow/OAuth2TokenClientHttpIntegration.test.ts +507 -0
- package/Tests/Server/Utils/Workflow/WorkflowVariableOAuthToken.test.ts +831 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +63 -1
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsAppAccessToken.test.ts +256 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +60 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChatActionAuthorization.test.ts +442 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +44 -2
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +55 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsReactionNoteSync.test.ts +1522 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsServiceUrl.test.ts +130 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +4 -4
- package/Tests/Server/Utils/Workspace/Slack/SlackActionAuthorization.test.ts +691 -0
- package/Tests/Server/Utils/Workspace/Slack/SlackMessageByTimestamp.test.ts +253 -0
- package/Tests/Server/Utils/Workspace/Slack/SlackReactionNote.test.ts +574 -0
- package/Tests/Server/Utils/Workspace/WorkspaceActionAuthorization.test.ts +488 -0
- package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +861 -0
- package/Tests/Server/Utils/Workspace/WorkspaceMessageAuthor.test.ts +635 -0
- package/Tests/Server/Utils/Workspace/WorkspaceOAuthState.test.ts +548 -0
- package/Tests/Server/Utils/Workspace/WorkspaceReactionNote.test.ts +839 -0
- package/Tests/Server/Utils/Workspace/WorkspaceSendErrorMessage.test.ts +725 -0
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +283 -9
- package/Tests/Types/DateBrowserLocalWallClock.test.ts +280 -0
- package/Tests/Types/DateLocalShortMonthNameFallback.test.ts +389 -0
- package/Tests/Types/DateUserTimezone.test.ts +177 -4
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +306 -0
- package/Tests/Types/Kubernetes/KubernetesClusterAiAccess.test.ts +283 -0
- package/Tests/Types/Kubernetes/KubernetesRunnerPostureWriteScope.test.ts +68 -0
- package/Tests/Types/Monitor/DatabaseMonitor/SqlServerPlatform.test.ts +144 -0
- package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +160 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +53 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +91 -16
- package/Tests/Types/Runner/RunnerLiveStatus.test.ts +14 -3
- package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +15 -0
- package/Tests/Types/StatusPage/PublicNoteSubscriberNotificationDefault.test.ts +1868 -0
- package/Tests/Types/Timezone.test.ts +5 -0
- package/Tests/Types/TimezoneAlias.test.ts +1034 -0
- package/Tests/Types/Workflow/JiraTemplateFixtures.ts +833 -0
- package/Tests/Types/Workflow/JiraTemplateScripts.test.ts +5414 -0
- package/Tests/Types/Workflow/JiraTemplateSimulation.test.ts +4803 -0
- package/Tests/Types/Workflow/JiraTemplates.test.ts +5429 -0
- package/Tests/Types/Workflow/Templates.test.ts +79 -4
- package/Tests/Types/Workflow/WorkflowVariableOAuth.test.ts +520 -0
- package/Tests/Types/Workspace/WorkspaceNoteReaction.test.ts +177 -0
- package/Tests/UI/Components/AttributesJSON/AttributesJSONView.test.tsx +390 -0
- package/Tests/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.test.tsx +566 -0
- package/Tests/UI/Components/AttributesJSON/JSONHighlight.test.ts +131 -0
- package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +276 -1
- package/Tests/UI/Components/Calendar/Calendar.test.tsx +1611 -0
- package/Tests/UI/Components/Calendar/CalendarDayLayout.test.ts +1486 -0
- package/Tests/UI/Components/Calendar/CalendarDayLayoutDst.test.ts +632 -0
- package/Tests/UI/Components/Calendar/CalendarDst.test.tsx +388 -0
- package/Tests/UI/Components/Charts/ChartGroupPlotFloor.test.tsx +170 -0
- package/Tests/UI/Components/Charts/HistogramSelection.test.ts +282 -0
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +6 -0
- package/Tests/UI/Components/CustomTimeRangeModalTimezoneTranslation.test.tsx +285 -0
- package/Tests/UI/Components/Dropdown.test.tsx +999 -2
- package/Tests/UI/Components/Forms/FormFieldCheckboxTranslation.test.tsx +216 -0
- package/Tests/UI/Components/Forms/FormFieldTimezoneDescriptionTranslation.test.tsx +461 -0
- package/Tests/UI/Components/Forms/IncidentEpisodePublicNoteNotifyPayload.test.tsx +641 -0
- package/Tests/UI/Components/Forms/IncidentPublicNoteNotifyPayload.test.tsx +346 -0
- package/Tests/UI/Components/Forms/ScheduledMaintenancePublicNoteNotifyPayload.test.tsx +493 -0
- package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +911 -2
- package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.NewYork.test.tsx +730 -0
- package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.Tokyo.test.tsx +506 -0
- package/Tests/UI/Components/HistogramClickToZoom.test.tsx +619 -0
- package/Tests/UI/Components/LogDetailsPanelAttributesJSON.test.tsx +248 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +47 -1
- package/Tests/UI/Components/LogsViewerSmallScreens.test.tsx +620 -0
- package/Tests/UI/Components/ModelPageModelChange.test.tsx +242 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableDeleteConfirmation.test.tsx +521 -0
- package/Tests/UI/Components/MonitorGraphs/Uptime.test.tsx +406 -2
- package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +169 -2
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +266 -0
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerSmallScreens.test.tsx +439 -0
- package/Tests/UI/Components/TimezoneDropdownLegacyValues.test.tsx +704 -0
- package/Tests/UI/Components/UseHistogramZoom.test.tsx +89 -1
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +175 -0
- package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +5 -5
- package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +28 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +51 -0
- package/Tests/UI/Utils/Timezone.test.ts +747 -33
- package/Tests/UI/Workspace/SendTestNotificationButton.test.tsx +1854 -0
- package/Tests/UI/Workspace/WorkspaceCardsSendTestNotification.test.tsx +1864 -0
- package/Tests/Utils/AI/InvestigationReport.test.ts +435 -7
- package/Tests/Utils/AiRemediation/KubectlAllowlistValidatorParity.test.ts +1084 -0
- package/Tests/Utils/AiRemediation/KubectlOutputRedactor.test.ts +2531 -0
- package/Tests/Utils/AiRemediation/KubectlPolicy.test.ts +1895 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyAllowlist.test.ts +428 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +828 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyCustomResourceScope.test.ts +573 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyDenials.test.ts +1525 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyExposeOverrides.test.ts +275 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyNodePatch.test.ts +428 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyObjectWords.test.ts +195 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyPatchParents.test.ts +637 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyPodSecurityBaseline.test.ts +322 -0
- package/Tests/Utils/AiRemediation/KubectlPolicySafeWriteScope.test.ts +1026 -0
- package/Tests/Utils/AiRemediation/KubectlWaitBudget.test.ts +267 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScope.test.ts +390 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +777 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeClusterScoped.test.ts +613 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeEntryPoint.test.ts +576 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeKubectlSpellings.test.ts +752 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeObjectWords.test.ts +358 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeOverrides.test.ts +293 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeParityCases.ts +598 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopePolicyParity.test.ts +403 -0
- package/Tests/Utils/Dashboard/DashboardClockComponent.test.ts +154 -5
- package/Tests/Utils/Monitor/IncomingEmailMonitorAddress.test.ts +478 -0
- package/Tests/Utils/Monitor/MonitorCheckScheduleUtil.test.ts +646 -0
- package/Tests/Utils/Monitor/MonitorOverviewCriteriaUtil.test.ts +292 -0
- package/Tests/Utils/Monitor/MonitorOverviewFamily.test.ts +274 -0
- package/Tests/Utils/Monitor/MonitorOverviewPresentationUtil.test.ts +3100 -0
- package/Tests/Utils/Monitor/MonitorOverviewProbeUtil.test.ts +1412 -0
- package/Tests/Utils/Monitor/MonitorOverviewTargetUtil.test.ts +374 -0
- package/Tests/Utils/Monitor/MonitorStatusHistoryUtil.test.ts +332 -0
- package/Tests/Utils/Monitor/MonitorUptimeSummaryUtil.test.ts +946 -0
- package/Tests/Utils/Rum/SessionIdentity.test.ts +94 -0
- package/Tests/Utils/Rum/SessionReplayWafCompatibility.test.ts +840 -0
- package/Tests/Utils/Rum/SessionReplayWireEncoding.test.ts +208 -0
- package/Tests/Utils/Rum/UserFlow.test.ts +1010 -0
- package/Tests/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.test.ts +217 -0
- package/Tests/Utils/StatusPage/ResourceUptime.test.ts +1272 -1
- package/Tests/Utils/StatusPage/UptimeDailyAggregateTotals.test.ts +298 -0
- package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +649 -1
- package/Tests/Utils/StatusPage/UptimeDailyAggregateWindow.test.ts +314 -0
- package/Tests/Utils/Telemetry/AttributesJSON.test.ts +454 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +418 -2
- package/Tests/Utils/Uptime/DayUptimeGraphUtilMidnightDst.test.ts +160 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Auckland.test.ts +45 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Kolkata.test.ts +40 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.London.test.ts +147 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.NewYork.test.ts +48 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Tokyo.test.ts +39 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezoneCases.ts +1015 -0
- package/Tests/Utils/Uptime/UptimeUtil.test.ts +75 -0
- package/Tests/Utils/Uptime/UptimeUtilChronologicalOrder.test.ts +1279 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +203 -5
- package/Types/Date.ts +121 -7
- package/Types/Email/EmailMessage.ts +8 -0
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/Types/Exception/ExceptionMessages.ts +1 -0
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +488 -0
- package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +54 -0
- package/Types/Kubernetes/KubernetesClusterAiAccessToolNames.ts +17 -0
- package/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.ts +9 -0
- package/Types/Monitor/DatabaseMonitor/SqlServerPlatform.ts +117 -0
- package/Types/Monitor/KubernetesAlertTemplates.ts +6 -5
- package/Types/Monitor/MonitorStepExceptionMonitor.ts +56 -0
- package/Types/Monitor/MonitorUptimeSummary.ts +113 -0
- package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +11 -0
- package/Types/Rum/SessionReplay.ts +55 -2
- package/Types/Rum/SessionReplayApi.ts +44 -1
- package/Types/Rum/UserFlow.ts +187 -0
- package/Types/Runbook/RunbookStepType.ts +10 -0
- package/Types/Runbook/RunnerJobOrigin.ts +15 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +6 -2
- package/Types/StatusPage/MergedDowntimeTotals.ts +17 -0
- package/Types/StatusPage/PublicNoteSubscriberNotificationDefault.ts +148 -0
- package/Types/StatusPage/UptimeDailyAggregate.ts +15 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +9 -0
- package/Types/Timezone.ts +5 -0
- package/Types/TimezoneAlias.ts +385 -0
- package/Types/Workflow/Templates.ts +3041 -2
- package/Types/Workflow/WorkflowVariableOAuth.ts +476 -0
- package/Types/Workspace/WorkspaceNoteReaction.ts +132 -0
- package/Typings/Index.d.ts +24 -0
- package/UI/Components/AttributesJSON/AttributesJSONPreferences.ts +175 -0
- package/UI/Components/AttributesJSON/AttributesJSONView.tsx +138 -0
- package/UI/Components/AttributesJSON/AttributesViewToggle.tsx +79 -0
- package/UI/Components/AttributesJSON/BracesGlyph.tsx +30 -0
- package/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.tsx +491 -0
- package/UI/Components/AttributesJSON/JSONHighlight.ts +77 -0
- package/UI/Components/Calendar/Calendar.css +83 -0
- package/UI/Components/Calendar/Calendar.tsx +136 -7
- package/UI/Components/Calendar/CalendarDayLayout.ts +236 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -1
- package/UI/Components/Charts/Utils/HistogramSelection.ts +117 -0
- package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +269 -0
- package/UI/Components/Charts/Utils/useHistogramZoom.ts +26 -2
- package/UI/Components/Date/CustomTimeRangeModal.tsx +26 -3
- package/UI/Components/Dropdown/Dropdown.tsx +58 -9
- package/UI/Components/Forms/Fields/FormField.tsx +49 -14
- package/UI/Components/Graphs/DayUptimeGraph.tsx +166 -25
- package/UI/Components/Graphs/UptimeBarTooltip.tsx +10 -2
- package/UI/Components/Graphs/UptimeDaySummary.tsx +8 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +52 -5
- package/UI/Components/LogsViewer/components/ColumnSelector.tsx +13 -2
- package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +13 -1
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +91 -76
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +15 -1
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +28 -106
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +54 -6
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +8 -2
- package/UI/Components/LogsViewer/useLogsHistogram.ts +39 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +66 -14
- package/UI/Components/MonitorGraphs/Uptime.tsx +12 -0
- package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +10 -2
- package/UI/Components/Page/ModelPage.tsx +59 -3
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +69 -5
- package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +12 -1
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +15 -1
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +36 -104
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +9 -1
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +19 -1
- package/UI/Utils/Timezone.ts +69 -28
- package/Utils/AI/InvestigationReport.ts +195 -25
- package/Utils/AiRemediation/KubectlOutputRedactor.ts +1724 -0
- package/Utils/AiRemediation/KubectlPolicy.ts +4136 -0
- package/Utils/AiRemediation/KubectlWaitBudget.ts +156 -0
- package/Utils/AiRemediation/KubectlWriteScope.ts +1964 -0
- package/Utils/Dashboard/Components/DashboardClockComponent.ts +7 -6
- package/Utils/Monitor/IncomingEmailMonitorAddress.ts +292 -0
- package/Utils/Monitor/MonitorCheckScheduleUtil.ts +654 -0
- package/Utils/Monitor/MonitorOverviewCriteriaUtil.ts +188 -0
- package/Utils/Monitor/MonitorOverviewFamily.ts +263 -0
- package/Utils/Monitor/MonitorOverviewPresentationUtil.ts +1958 -0
- package/Utils/Monitor/MonitorOverviewProbeUtil.ts +979 -0
- package/Utils/Monitor/MonitorOverviewTargetUtil.ts +299 -0
- package/Utils/Monitor/MonitorStatusHistoryUtil.ts +201 -0
- package/Utils/Monitor/MonitorUptimeSummaryUtil.ts +638 -0
- package/Utils/Rum/SessionIdentity.ts +44 -0
- package/Utils/Rum/SessionReplayWireEncoding.ts +137 -0
- package/Utils/Rum/UserFlow.ts +1527 -0
- package/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.ts +107 -0
- package/Utils/StatusPage/ResourceUptime.ts +99 -0
- package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +197 -0
- package/Utils/Telemetry/AttributesJSON.ts +367 -0
- package/Utils/Uptime/DayUptimeGraphUtil.ts +165 -0
- package/Utils/Uptime/UptimeUtil.ts +122 -38
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +8 -1
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +8 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +10 -3
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +10 -3
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +434 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +49 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Runner.js +4 -4
- package/build/dist/Models/DatabaseModels/Runner.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +66 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +11 -3
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js +487 -3
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js +64 -16
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +205 -0
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +115 -2
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +49 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +564 -0
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -0
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +465 -176
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/MonitorAPI.js +107 -1
- package/build/dist/Server/API/MonitorAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +5 -16
- package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +322 -78
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +123 -17
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +144 -5
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +1 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -1
- package/build/dist/Server/API/WorkflowVariableAPI.js +109 -0
- package/build/dist/Server/API/WorkflowVariableAPI.js.map +1 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js +29 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js +54 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js +32 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +21 -0
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +32 -2
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +39 -6
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/AIAgentService.js +1 -0
- package/build/dist/Server/Services/AIAgentService.js.map +1 -1
- package/build/dist/Server/Services/AccessTokenService.js +87 -42
- package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeMemberService.js +4 -0
- package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeService.js +12 -1
- package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +110 -23
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +716 -30
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js +100 -0
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +1 -0
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js +11 -16
- package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeMemberService.js +4 -0
- package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +48 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js +21 -0
- package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +12 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -0
- package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentMemberService.js +14 -0
- package/build/dist/Server/Services/IncidentMemberService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +48 -0
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +99 -21
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +68 -4
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +1470 -0
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesClusterService.js +1097 -0
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +14 -6
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +487 -48
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +540 -3
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +3 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js +29 -7
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -1
- package/build/dist/Server/Services/OnCallSetupReminderService.js +1 -0
- package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -1
- package/build/dist/Server/Services/ProbeService.js +7 -1
- package/build/dist/Server/Services/ProbeService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +11 -1
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RunbookCredentialService.js +24 -0
- package/build/dist/Server/Services/RunbookCredentialService.js.map +1 -1
- package/build/dist/Server/Services/RunbookSecretService.js +44 -0
- package/build/dist/Server/Services/RunbookSecretService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +641 -21
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +344 -5
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +48 -0
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -20
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js +67 -4
- package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +12 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -0
- package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +230 -32
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +4 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +270 -11
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +10 -6
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +1 -0
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +1 -0
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +4 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +107 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowVariableService.js +396 -7
- package/build/dist/Server/Services/WorkflowVariableService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +463 -7
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +144 -2
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +20 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +394 -27
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +123 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +344 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +587 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js +18 -2
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1380 -239
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +1099 -68
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +88 -6
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +45 -11
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +41 -11
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +11 -4
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +4 -2
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +997 -141
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +299 -4
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
- package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +120 -0
- package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -0
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +160 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +1 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +59 -12
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +5 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +84 -22
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +70 -5
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js +249 -0
- package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -1
- package/build/dist/Server/Utils/PushNotificationUtil.js +27 -0
- package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -1
- package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js +26 -3
- package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js.map +1 -1
- package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js +38 -3
- package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +143 -23
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js +406 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js.map +1 -0
- package/build/dist/Server/Utils/StartServer.js +4 -3
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +382 -0
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -0
- package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js +1 -0
- package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js.map +1 -1
- package/build/dist/Server/Utils/UserPermission/UserPermission.js +10 -0
- package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +447 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -0
- package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js +202 -0
- package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +39 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +44 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +150 -191
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +44 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js +39 -17
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +151 -145
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +221 -127
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js +91 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js +673 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js +3 -9
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +65 -166
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +63 -157
- package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +23 -3
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js +92 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +81 -210
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js +68 -138
- package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js +240 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js +85 -212
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +60 -17
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
- package/build/dist/Server/Utils/Workspace/Workspace.js +134 -41
- package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js +197 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceBase.js +32 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js +199 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js +91 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js +450 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +103 -5
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/Date.js +97 -7
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Exception/ExceptionMessages.js +1 -0
- package/build/dist/Types/Exception/ExceptionMessages.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +245 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +49 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js +16 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js.map +1 -0
- package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js +104 -0
- package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js.map +1 -0
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +6 -5
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js +32 -0
- package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorUptimeSummary.js +52 -0
- package/build/dist/Types/Monitor/MonitorUptimeSummary.js.map +1 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +9 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +52 -2
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayApi.js +11 -0
- package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
- package/build/dist/Types/Rum/UserFlow.js +115 -0
- package/build/dist/Types/Rum/UserFlow.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +10 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/Types/Runbook/RunnerJobOrigin.js +14 -0
- package/build/dist/Types/Runbook/RunnerJobOrigin.js.map +1 -1
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +2 -2
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -1
- package/build/dist/Types/StatusPage/MergedDowntimeTotals.js +2 -0
- package/build/dist/Types/StatusPage/MergedDowntimeTotals.js.map +1 -0
- package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js +65 -0
- package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +8 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/Types/Timezone.js +5 -0
- package/build/dist/Types/Timezone.js.map +1 -1
- package/build/dist/Types/TimezoneAlias.js +340 -0
- package/build/dist/Types/TimezoneAlias.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +2694 -2
- package/build/dist/Types/Workflow/Templates.js.map +1 -1
- package/build/dist/Types/Workflow/WorkflowVariableOAuth.js +314 -0
- package/build/dist/Types/Workflow/WorkflowVariableOAuth.js.map +1 -0
- package/build/dist/Types/Workspace/WorkspaceNoteReaction.js +98 -0
- package/build/dist/Types/Workspace/WorkspaceNoteReaction.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js +95 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js +44 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js +27 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js +9 -0
- package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js +260 -0
- package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js +56 -0
- package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js.map +1 -0
- package/build/dist/UI/Components/Calendar/Calendar.js +94 -7
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Calendar/CalendarDayLayout.js +136 -0
- package/build/dist/UI/Components/Calendar/CalendarDayLayout.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js +72 -0
- package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +178 -0
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js +13 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js.map +1 -1
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +20 -4
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +33 -9
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +36 -13
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +117 -11
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -1
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +36 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js +10 -2
- package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +9 -1
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +16 -15
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +14 -74
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +22 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +9 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +16 -5
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +23 -8
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js +7 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +5 -1
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
- package/build/dist/UI/Components/Page/ModelPage.js +37 -3
- package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +27 -6
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +7 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +17 -77
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +10 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +15 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/Timezone.js +52 -17
- package/build/dist/UI/Utils/Timezone.js.map +1 -1
- package/build/dist/Utils/AI/InvestigationReport.js +116 -21
- package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
- package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js +1209 -0
- package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js.map +1 -0
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js +2973 -0
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js +99 -0
- package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js.map +1 -0
- package/build/dist/Utils/AiRemediation/KubectlWriteScope.js +1429 -0
- package/build/dist/Utils/AiRemediation/KubectlWriteScope.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js +7 -6
- package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js.map +1 -1
- package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js +214 -0
- package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js +471 -0
- package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js +107 -0
- package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewFamily.js +191 -0
- package/build/dist/Utils/Monitor/MonitorOverviewFamily.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js +1259 -0
- package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js +593 -0
- package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js +195 -0
- package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js +127 -0
- package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js +464 -0
- package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js.map +1 -0
- package/build/dist/Utils/Rum/SessionIdentity.js +31 -1
- package/build/dist/Utils/Rum/SessionIdentity.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayWireEncoding.js +128 -0
- package/build/dist/Utils/Rum/SessionReplayWireEncoding.js.map +1 -0
- package/build/dist/Utils/Rum/UserFlow.js +941 -0
- package/build/dist/Utils/Rum/UserFlow.js.map +1 -0
- package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js +82 -0
- package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js.map +1 -0
- package/build/dist/Utils/StatusPage/ResourceUptime.js +46 -0
- package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
- package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
- package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/AttributesJSON.js +245 -0
- package/build/dist/Utils/Telemetry/AttributesJSON.js.map +1 -0
- package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +103 -0
- package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -1
- package/build/dist/Utils/Uptime/UptimeUtil.js +91 -27
- package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
- package/jest.config.json +2 -1
- package/package.json +2 -1
- package/tsconfig.tests.json +36 -0
- package/Tests/UI/Components/EmptyState/EmptyStateGuideLinks.test.tsx +0 -337
- package/UI/Components/EmptyState/EmptyStateGuideLinks.tsx +0 -124
- package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js +0 -47
- package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js.map +0 -1
|
@@ -0,0 +1,4136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KubectlCommandTier,
|
|
3
|
+
isProtectedKubernetesNamespace,
|
|
4
|
+
} from "../../Types/Kubernetes/KubernetesClusterAiAccess";
|
|
5
|
+
import {
|
|
6
|
+
AiRemediationCommandPolicyVerdict,
|
|
7
|
+
MAX_COMMAND_LENGTH_CHARS,
|
|
8
|
+
} from "../../Types/AutoRemediation/AiRemediationCommandPlan";
|
|
9
|
+
import CommandPolicy from "./CommandPolicy";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Policy gate for kubectl commands OneUptime AI composes against a cluster.
|
|
13
|
+
*
|
|
14
|
+
* Pure functions with no server dependencies on purpose: the same evaluation
|
|
15
|
+
* runs where the AI composes the command, at the server-side enqueue
|
|
16
|
+
* chokepoint, and inside the customer-side Runner right before it spawns
|
|
17
|
+
* kubectl. The Runner is the last line: a compromised server still cannot
|
|
18
|
+
* make it run a Denied command.
|
|
19
|
+
*
|
|
20
|
+
* kubectl is NEVER invoked through a shell. A command is tokenized into an
|
|
21
|
+
* argv here (shell quoting only, no expansion, no operators) and the Runner
|
|
22
|
+
* spawns the kubectl binary with that argv, so `;`, `|`, `$( )` and friends
|
|
23
|
+
* are literal bytes in an argument, not a second command. What this policy
|
|
24
|
+
* has to bound is therefore what kubectl itself can do, which is a closed
|
|
25
|
+
* vocabulary of verbs and flags — a much better place to stand than a bash
|
|
26
|
+
* denylist.
|
|
27
|
+
*
|
|
28
|
+
* Tiers (see KubectlCommandTier): Read is what an investigation may run;
|
|
29
|
+
* SafeWrite is what Automatic remediation runs unattended; RiskyWrite needs
|
|
30
|
+
* a human unless the operator allowlisted it (or the cluster bypasses
|
|
31
|
+
* approvals); Denied never runs.
|
|
32
|
+
*
|
|
33
|
+
* SafeWrite is deliberately narrow: exactly ONE named object of one
|
|
34
|
+
* built-in kind, with no selector (-l, --selector or --field-selector) and
|
|
35
|
+
* no --all —
|
|
36
|
+
* - rollout restart/undo/pause/resume of one Deployment, StatefulSet or
|
|
37
|
+
* DaemonSet (a bare kind is every object of that kind to kubectl);
|
|
38
|
+
* - scale of one Deployment, StatefulSet or ReplicaSet to a count above
|
|
39
|
+
* zero (zero is an outage, not a nudge);
|
|
40
|
+
* - delete of one named pod (its controller recreates it; a Job is not
|
|
41
|
+
* recreated by anything, so deleting one is RiskyWrite);
|
|
42
|
+
* - cordon/uncordon of one node;
|
|
43
|
+
* - label/annotate of one pod or workload (Deployment, StatefulSet,
|
|
44
|
+
* DaemonSet, ReplicaSet, Job, CronJob) with keys outside the reserved
|
|
45
|
+
* kubernetes.io / k8s.io prefixes (and a few identity, admission and
|
|
46
|
+
* GitOps controller prefixes). Labels on namespaces, nodes, Services,
|
|
47
|
+
* RBAC objects or webhooks steer admission, scheduling, identity or
|
|
48
|
+
* traffic, so they are RiskyWrite.
|
|
49
|
+
* Anything wider is RiskyWrite. A write whose namespace is one of
|
|
50
|
+
* PROTECTED_KUBERNETES_NAMESPACES (kube-system, kube-public,
|
|
51
|
+
* kube-node-lease), or that targets one of those Namespace objects, is at
|
|
52
|
+
* least RiskyWrite and carries `protectedNamespace`: evaluateForAutoExecution
|
|
53
|
+
* never runs it without a human, whatever the mode or allowlist. A write's
|
|
54
|
+
* namespace is the one -n names (or the context's), except for the built-in
|
|
55
|
+
* cluster-scoped kinds — nodes, Namespace objects, PersistentVolumes, ... —
|
|
56
|
+
* which kubectl writes outside any namespace. Those count only in their own
|
|
57
|
+
* API group, the way kubectl resolves them (builtinKind()): a custom
|
|
58
|
+
* resource that borrows a built-in name, such as `nodes.example.com` or
|
|
59
|
+
* `namespaces.example.com`, has a scope this policy cannot see, so it is
|
|
60
|
+
* judged by the namespace -n names — `annotate nodes.example.com x note=y
|
|
61
|
+
* -n kube-system` is a kube-system write. And a write to Namespace objects
|
|
62
|
+
* must name each one (see Denied): the rule reads their names.
|
|
63
|
+
*
|
|
64
|
+
* `kubectl drain` and `kubectl taint` are RiskyWrite and carry
|
|
65
|
+
* `requiresHuman` for the same reason. A drain evicts every pod on the node
|
|
66
|
+
* and a taint decides which pods the node keeps (a NoExecute taint evicts
|
|
67
|
+
* the rest), in EVERY namespace — kube-system and the OneUptime agent's own
|
|
68
|
+
* included — although the command names no namespace at all. So, like a
|
|
69
|
+
* protected-namespace write, evaluateForAutoExecution never runs one without
|
|
70
|
+
* a human: not in Bypass approval, not through the allowlist. cordon and
|
|
71
|
+
* uncordon only stop or resume scheduling on the node and stay SafeWrite.
|
|
72
|
+
*
|
|
73
|
+
* Every `kubectl patch` of a Node carries `requiresHuman` too, whatever its
|
|
74
|
+
* body says. A taint is nothing but the Node's spec.taints, and a patch
|
|
75
|
+
* writes it as surely as `kubectl taint` does (checked with the pinned
|
|
76
|
+
* kubectl v1.36.4 and `patch --local`: a strategic-merge list replaces
|
|
77
|
+
* every taint the node had, a merge-patch null clears them, a JSON-patch add
|
|
78
|
+
* appends one) — the substitute a model reaches for once `taint` is refused.
|
|
79
|
+
* The rule reads the KIND, not the body: a body check would have to follow
|
|
80
|
+
* every patch type, directive and JSON-patch copy or move to spec.taints,
|
|
81
|
+
* and no fix needs a raw node patch unattended (cordon and uncordon change
|
|
82
|
+
* scheduling, label and annotate change metadata). It counts every spelling
|
|
83
|
+
* of the built-in Node that kubectl resolves — node, nodes, no, Node,
|
|
84
|
+
* nodes.v1., no/NAME, several names, a comma list, and a Node after another
|
|
85
|
+
* object whose name holds an `=` (`patch pod/a=b node/n1`: patch reads every
|
|
86
|
+
* word as an object, see objectTokens) — through builtinKind(), so a custom
|
|
87
|
+
* resource that borrows the name (`nodes.example.com`) is not a Node: it
|
|
88
|
+
* cannot taint anything, and is RiskyWrite like any other custom resource.
|
|
89
|
+
* `kubectl label node` and `kubectl annotate node` stay RiskyWrite without
|
|
90
|
+
* it: they write metadata only.
|
|
91
|
+
*
|
|
92
|
+
* Denied is what no one should be asked to approve from an AI plan:
|
|
93
|
+
* - Changes to who may do what, or to what the API server admits and
|
|
94
|
+
* serves: EVERY write (label, annotate, patch, set, scale, rollout,
|
|
95
|
+
* delete, ...) on the RBAC kinds (roles, cluster roles and their
|
|
96
|
+
* bindings) and on the admission and API-extension kinds (validating /
|
|
97
|
+
* mutating webhook configurations, validating / mutating admission
|
|
98
|
+
* policies and their bindings, APIServices, CRDs), in every spelling
|
|
99
|
+
* kubectl accepts; creating RBAC objects or ServiceAccounts; set
|
|
100
|
+
* subject; set serviceaccount; auth reconcile; certificate approve/deny.
|
|
101
|
+
* Reading these kinds stays Read.
|
|
102
|
+
* - Wiring identity, privileges, host access, Secrets or a new program
|
|
103
|
+
* into a pod: patch bodies touching POD_SECURITY_PATCH_KEYS (every Pod
|
|
104
|
+
* Security Standards baseline control among them: host namespaces,
|
|
105
|
+
* host ports, hostPath, privileges and the rest of securityContext, the
|
|
106
|
+
* AppArmor annotation and a probe's or lifecycle hook's host; and the
|
|
107
|
+
* user-namespace opt-out, the runtime class and process-namespace
|
|
108
|
+
* sharing) — by name, or by replacing or removing what holds them
|
|
109
|
+
* (a strategic-merge `$patch`, `$retainKeys` or `$setElementOrder` on
|
|
110
|
+
* a pod spec or above it, a merge patch that sets the containers list
|
|
111
|
+
* wholesale, a JSON-patch replace or remove of /spec/template/spec or
|
|
112
|
+
* of a container; see POD_SPEC_PATH_STEPS) — Pod Security Admission
|
|
113
|
+
* labels, set env --from=secret/..., and creating a
|
|
114
|
+
* workload that runs an image named in the command — `create
|
|
115
|
+
* deployment` and `create cronjob` (kubectl requires --image for both)
|
|
116
|
+
* and `create job --image`. Only `create job NAME --from=cronjob/NAME`
|
|
117
|
+
* stays, as RiskyWrite: it re-runs the CronJob's existing spec. `set
|
|
118
|
+
* image` is different on purpose: it changes the image of a workload
|
|
119
|
+
* that already exists, under the identity, security context and mounts
|
|
120
|
+
* that workload already has, and a rollback puts the old image back —
|
|
121
|
+
* so it is RiskyWrite, and a human, the allowlist or Bypass approval
|
|
122
|
+
* decides.
|
|
123
|
+
* - kubectl expose with --overrides or --override-type, in every spelling
|
|
124
|
+
* kubectl accepts (--overrides=..., --overrides ..., --override_type).
|
|
125
|
+
* kubectl merges the override into the Service it generates and then
|
|
126
|
+
* creates whatever object the result describes, of any kind and in any
|
|
127
|
+
* namespace: checked with the pinned kubectl v1.36.4 against a fake API
|
|
128
|
+
* server, `expose deployment web -n prod --overrides=...` POSTs a
|
|
129
|
+
* cluster-admin ClusterRoleBinding, and `--override_type=json` with a
|
|
130
|
+
* `replace /kind` makes it a Namespace. So the command no longer says
|
|
131
|
+
* what it creates — an RBAC grant, a PriorityClass, a Job with any
|
|
132
|
+
* image and pod spec — and the write scope, the RBAC rule and the patch
|
|
133
|
+
* rules above all read the wrong object. expose is the only verb
|
|
134
|
+
* OneUptime AI may run that takes these flags (kubectl run, the one
|
|
135
|
+
* other v1.36.4 command that does, is a Denied verb); they are refused
|
|
136
|
+
* on every write verb all the same (OBJECT_OVERRIDE_FLAGS).
|
|
137
|
+
* - A patch body that is not JSON. kubectl reads any other body as YAML
|
|
138
|
+
* 1.1, whose tags (`!!binary`), anchors, aliases and merge keys can
|
|
139
|
+
* spell a field name that appears nowhere in the text, so a body is
|
|
140
|
+
* only ever checked — and only ever run — as JSON (see
|
|
141
|
+
* findPatchBodyProblem).
|
|
142
|
+
* - Deleting cluster-level kinds (NEVER_DELETE_KINDS) in any spelling.
|
|
143
|
+
* - A write to Namespace objects that does not name each one — by a
|
|
144
|
+
* selector (`label ns -l env=prod`, `annotate ns --field-selector ...`),
|
|
145
|
+
* --all (`label ns --all`) or a bare kind. It can change kube-system's
|
|
146
|
+
* Namespace object without naming it, where the protected-namespace
|
|
147
|
+
* rule, which reads the names, cannot see it. Only the built-in
|
|
148
|
+
* Namespace kind counts (builtinKind()).
|
|
149
|
+
* - Anything that names a resource CATEGORY (all, api-extensions, ...)
|
|
150
|
+
* in a write, because kubectl expands a category into many kinds before
|
|
151
|
+
* this policy could see them.
|
|
152
|
+
* - Anything kubectl would not run as the command we read — a verb or
|
|
153
|
+
* create/set subcommand it does not have (kubectl would run a
|
|
154
|
+
* kubectl-<word> plugin from the Runner's PATH), one not written in
|
|
155
|
+
* lowercase (cobra matches case-sensitively), or a namespace set twice
|
|
156
|
+
* (pflag keeps the last one).
|
|
157
|
+
*
|
|
158
|
+
* Three more things are Denied because their OUTPUT is the problem, not the
|
|
159
|
+
* verb. Secret objects are off-limits in every verb (get, describe, label,
|
|
160
|
+
* patch, delete, create secret, create token, set env --resolve), wherever
|
|
161
|
+
* the command names one among its objects (`get pod/a=b secret/s` names one:
|
|
162
|
+
* only label, annotate, taint, set image and set env read a word with an `=`
|
|
163
|
+
* as an update; see objectTokens): each of those can print or mint
|
|
164
|
+
* credential values, which would land in front of the model and in the job
|
|
165
|
+
* record. File-backed output formats (-o
|
|
166
|
+
* jsonpath-file=..., --template) make kubectl read a file off the Runner and
|
|
167
|
+
* echo it. And a flag written before the verb is refused unless it is one of
|
|
168
|
+
* kubectl's global flags, because kubectl picks the command before it parses
|
|
169
|
+
* flags and `kubectl --all events delete` runs delete (see the Flags section).
|
|
170
|
+
*
|
|
171
|
+
* ---- The cluster kubectl allowlist -------------------------------------------
|
|
172
|
+
*
|
|
173
|
+
* The AI page's "kubectl allowlist" (KubernetesCluster.aiKubectlCommandAllowlist)
|
|
174
|
+
* lets an operator pre-approve RiskyWrite SHAPES in Automatic mode. It is
|
|
175
|
+
* matched token by token against the argv kubectl will get, never as one
|
|
176
|
+
* glob over the rendered string (see matchesAllowlist):
|
|
177
|
+
* - A pattern is tokenized exactly like a command: shell quoting, and a
|
|
178
|
+
* leading "kubectl" (in any case) is optional, in a pattern exactly as
|
|
179
|
+
* in a command — "kubectl set image ..." and "set image ..." are the
|
|
180
|
+
* same entry. It must have the SAME number of tokens as the command, so
|
|
181
|
+
* `*` can never absorb an extra resource, a selector, a second -n or
|
|
182
|
+
* any other extra argument.
|
|
183
|
+
* - Each pattern token matches exactly one argv token. A `*` inside a
|
|
184
|
+
* token is a glob within that one token and never matches whitespace:
|
|
185
|
+
* `web=*` matches `web=nginx:1.27`, `deployment/*` matches
|
|
186
|
+
* `deployment/api`, and `-p *` matches a patch body written without
|
|
187
|
+
* spaces. A `*` is one word, so a leading `*` stands for the verb,
|
|
188
|
+
* never for "kubectl".
|
|
189
|
+
* - Flags must be spelled in the pattern to be present in the command: a
|
|
190
|
+
* command token that is a flag is only matched by a pattern token that
|
|
191
|
+
* spells the same flag literally (`-n`, `--replicas=*`); a `*` never
|
|
192
|
+
* stands for a flag.
|
|
193
|
+
* - Case-sensitive. The first 100 non-blank entries are read.
|
|
194
|
+
* - The allowlist never promotes a Denied command, never a write in a
|
|
195
|
+
* protected namespace, and never a drain, a taint or a patch of a Node.
|
|
196
|
+
*
|
|
197
|
+
* Which entries are valid is decided in ONE place,
|
|
198
|
+
* describeAllowlistPatternProblem: the server refuses to save anything
|
|
199
|
+
* else, the AI page refuses it in the form, and matchesAllowlist skips it —
|
|
200
|
+
* so an entry either works as written or is refused where it is typed. An
|
|
201
|
+
* entry is valid when it is:
|
|
202
|
+
* - a non-blank string of at most 500 characters, on one line, with
|
|
203
|
+
* balanced quotes and at most 64 words (a bare "kubectl" names no
|
|
204
|
+
* command);
|
|
205
|
+
* - of at least two words after the optional "kubectl": a one-word entry
|
|
206
|
+
* ("kubectl delete", "*", or a full command in quotes, as in a pasted
|
|
207
|
+
* JSON list) could only match a one-word command, and none is ever
|
|
208
|
+
* pre-approved — a read runs anyway, and kubectl refuses a write verb
|
|
209
|
+
* with nothing to act on;
|
|
210
|
+
* - that names its kubectl verb written out: lowercase, no `*` — and, for
|
|
211
|
+
* rollout, set and create (and auth, cluster-info and top when it names
|
|
212
|
+
* one), its subcommand the same way. A wildcard verb or subcommand
|
|
213
|
+
* would pre-approve every KIND of change at once, and a leading `*`
|
|
214
|
+
* written for "kubectl" (the old whole-command glob) never matched
|
|
215
|
+
* anything;
|
|
216
|
+
* - whose verb is one OneUptime AI may run at all; and
|
|
217
|
+
* - whose flags the policy reads: a flag this policy refuses (--overrides
|
|
218
|
+
* and --override-type among them) or does not know, or a non-global
|
|
219
|
+
* flag before the verb, makes every command the entry could match
|
|
220
|
+
* Denied.
|
|
221
|
+
*
|
|
222
|
+
* An entry is BROAD (isBroadAllowlistPattern; the AI page asks for an
|
|
223
|
+
* explicit confirmation before saving one) when a `*` stands for the
|
|
224
|
+
* namespace, for an object the command acts on (a kind, a name, a
|
|
225
|
+
* TYPE/NAME, or an extra object ahead of the update), or for a selector or
|
|
226
|
+
* --from source. A `*` in a VALUE keeps the entry about the objects it
|
|
227
|
+
* names: the image pair in the documented `kubectl set image deployment/web
|
|
228
|
+
* * -n web` (kubectl reads that last word either as the image update or as
|
|
229
|
+
* another object with no update, which it refuses), a patch body, a replica
|
|
230
|
+
* count, a revision, or the name of the Job `create job * --from=cronjob/X`
|
|
231
|
+
* makes. Entries the allowlist can never promote — reads, drain, taint and a
|
|
232
|
+
* patch that names a Node (`kubectl patch node * -p *`) — pre-approve
|
|
233
|
+
* nothing and are never broad.
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
export interface KubectlPolicyResult {
|
|
237
|
+
tier: KubectlCommandTier;
|
|
238
|
+
// Human-readable: why this tier. Shown to the model and on approval cards.
|
|
239
|
+
reason: string;
|
|
240
|
+
// Normalized argv WITHOUT the leading "kubectl".
|
|
241
|
+
args: Array<string>;
|
|
242
|
+
// "get", "rollout restart", "delete pod", ... — for audit labels.
|
|
243
|
+
verb: string;
|
|
244
|
+
// The exact command, rendered for humans: "kubectl get pods -n web".
|
|
245
|
+
displayCommand: string;
|
|
246
|
+
/*
|
|
247
|
+
* The namespace the command names with -n / --namespace (-nX, -n=X,
|
|
248
|
+
* --namespace=X), exactly as kubectl parses it. Absent when the command
|
|
249
|
+
* gives none — kubectl then uses the context's namespace (the Runner pod's
|
|
250
|
+
* own, in-cluster). Never set on a Denied result.
|
|
251
|
+
*/
|
|
252
|
+
namespace?: string | undefined;
|
|
253
|
+
/*
|
|
254
|
+
* Set on a write that lands in, or targets, one of
|
|
255
|
+
* PROTECTED_KUBERNETES_NAMESPACES. Such a write is at least RiskyWrite and
|
|
256
|
+
* evaluateForAutoExecution never runs it without a human: not with
|
|
257
|
+
* bypassApproval, not through the allowlist.
|
|
258
|
+
*/
|
|
259
|
+
protectedNamespace?: string | undefined;
|
|
260
|
+
/*
|
|
261
|
+
* Set on a node drain, a taint and a patch of a Node (see the header).
|
|
262
|
+
* Such a command is RiskyWrite and evaluateForAutoExecution never runs it
|
|
263
|
+
* without a human, exactly like a protected-namespace write: it can move
|
|
264
|
+
* pods in every namespace, kube-system and the agent's own included.
|
|
265
|
+
*/
|
|
266
|
+
requiresHuman?: boolean | undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* kubectl normalizes "_" to "-" in long flag names (pflag's
|
|
271
|
+
* WordSepNormalizeFunc), so --as_group IS --as-group and
|
|
272
|
+
* --insecure_skip_tls_verify IS --insecure-skip-tls-verify. Every flag
|
|
273
|
+
* lookup here goes through this first, so an underscore spelling can never
|
|
274
|
+
* slip past a deny list; anything else that inspects kubectl flags (the
|
|
275
|
+
* Runner's argv guard) should use it too.
|
|
276
|
+
*/
|
|
277
|
+
export function normalizeKubectlFlagName(name: string): string {
|
|
278
|
+
return (name || "").replace(/_/g, "-");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface KubectlAutoExecutionVerdict {
|
|
282
|
+
verdict: AiRemediationCommandPolicyVerdict;
|
|
283
|
+
tier: KubectlCommandTier;
|
|
284
|
+
reason: string;
|
|
285
|
+
/*
|
|
286
|
+
* True when nothing an operator can configure lets this command run
|
|
287
|
+
* unattended — a write in a protected namespace, or a node drain, taint
|
|
288
|
+
* or patch — so an approval card should say that (the reason does), not
|
|
289
|
+
* that the cluster "only runs safe changes". Absent otherwise.
|
|
290
|
+
*/
|
|
291
|
+
requiresHuman?: boolean | undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface KubectlTokenizeResult {
|
|
295
|
+
args?: Array<string> | undefined;
|
|
296
|
+
errorMessage?: string | undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const MAX_TOKENS: number = 64;
|
|
300
|
+
|
|
301
|
+
// Tokens made only of these survive a shell split unquoted.
|
|
302
|
+
const SHELL_SAFE_TOKEN: RegExp = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
303
|
+
|
|
304
|
+
/*
|
|
305
|
+
* ---- Flags -----------------------------------------------------------------
|
|
306
|
+
*
|
|
307
|
+
* kubectl parses flags with pflag, and this policy has to see the SAME
|
|
308
|
+
* positionals kubectl will see, or a resource kind can hide inside what we
|
|
309
|
+
* took for a flag value, and a flag value can pose as the verb. pflag's
|
|
310
|
+
* rules, which parseArgs() mirrors exactly:
|
|
311
|
+
*
|
|
312
|
+
* - "--flag=value" carries its value inline. "--flag value" carries it in
|
|
313
|
+
* the next token ONLY when the flag takes a value: a boolean flag never
|
|
314
|
+
* consumes the next token, and neither does an optional-value flag
|
|
315
|
+
* ("--cascade" alone means background; "--cascade=orphan" is a value).
|
|
316
|
+
* - "-abc" is a cluster of short flags read letter by letter: each letter
|
|
317
|
+
* is a boolean flag until one that takes a value, whose value is the
|
|
318
|
+
* rest of the token ("-nweb") or, when nothing is left, the next token
|
|
319
|
+
* ("-n web"). "-n=web" works too.
|
|
320
|
+
* - A value flag takes the next token whatever it looks like: "-n -A" is
|
|
321
|
+
* the namespace "-A".
|
|
322
|
+
* - "--" ends flag parsing; everything after it is positional.
|
|
323
|
+
* - "_" in a long flag name reads as "-" ("--all_namespaces"); see
|
|
324
|
+
* normalizeKubectlFlagName, which runs before every lookup.
|
|
325
|
+
* - A flag kubectl does not know is an error.
|
|
326
|
+
* - The command is picked BEFORE flags are parsed (by cobra, not pflag),
|
|
327
|
+
* and at that point any flag that is not a global flag is assumed to
|
|
328
|
+
* take the next word as its value, whatever the verb makes of it later:
|
|
329
|
+
* `kubectl --all events delete -n web` runs `delete --all events`, and
|
|
330
|
+
* `kubectl cluster-info -A x dump` runs `cluster-info dump -A`. So only
|
|
331
|
+
* GLOBAL_FLAGS may precede the verb (and, for SUBCOMMAND_VERBS, the
|
|
332
|
+
* subcommand); every other flag must follow it. kubectl itself rejects
|
|
333
|
+
* most of the other orderings ("flags cannot be placed before plugin
|
|
334
|
+
* name"), so nothing that could have run is lost.
|
|
335
|
+
*
|
|
336
|
+
* Every flag OneUptime AI may use is listed here with its arity. A flag that
|
|
337
|
+
* is not listed is Denied: without knowing whether it swallows the next
|
|
338
|
+
* token we cannot know which token is the verb or the kind, so it is refused
|
|
339
|
+
* rather than guessed. kubectl rejects flags it does not know, so nothing
|
|
340
|
+
* that could have run is lost.
|
|
341
|
+
*/
|
|
342
|
+
enum FlagArity {
|
|
343
|
+
// Never takes a value: "--all", "-A". ("--all=false" is the only value form.)
|
|
344
|
+
Boolean = "Boolean",
|
|
345
|
+
// Always takes a value: "-n web", "-nweb", "-n=web", "--namespace web".
|
|
346
|
+
Value = "Value",
|
|
347
|
+
/*
|
|
348
|
+
* Takes a value only when written "--flag=value". Written bare it means
|
|
349
|
+
* its default and never consumes the next token (pflag's NoOptDefVal).
|
|
350
|
+
*/
|
|
351
|
+
OptionalValue = "OptionalValue",
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const KNOWN_FLAGS: Record<string, FlagArity> = {
|
|
355
|
+
// Global (any verb).
|
|
356
|
+
n: FlagArity.Value,
|
|
357
|
+
namespace: FlagArity.Value,
|
|
358
|
+
"request-timeout": FlagArity.Value,
|
|
359
|
+
"match-server-version": FlagArity.Boolean,
|
|
360
|
+
|
|
361
|
+
// Output and table shaping. The -o value itself is checked separately.
|
|
362
|
+
o: FlagArity.Value,
|
|
363
|
+
output: FlagArity.Value,
|
|
364
|
+
"allow-missing-template-keys": FlagArity.Boolean,
|
|
365
|
+
"show-labels": FlagArity.Boolean,
|
|
366
|
+
"show-kind": FlagArity.Boolean,
|
|
367
|
+
"show-managed-fields": FlagArity.Boolean,
|
|
368
|
+
"no-headers": FlagArity.Boolean,
|
|
369
|
+
"server-print": FlagArity.Boolean,
|
|
370
|
+
"ignore-not-found": FlagArity.Boolean,
|
|
371
|
+
"output-watch-events": FlagArity.Boolean,
|
|
372
|
+
L: FlagArity.Value,
|
|
373
|
+
"label-columns": FlagArity.Value,
|
|
374
|
+
"sort-by": FlagArity.Value,
|
|
375
|
+
"chunk-size": FlagArity.Value,
|
|
376
|
+
subresource: FlagArity.Value,
|
|
377
|
+
|
|
378
|
+
// Selecting objects.
|
|
379
|
+
A: FlagArity.Boolean,
|
|
380
|
+
"all-namespaces": FlagArity.Boolean,
|
|
381
|
+
all: FlagArity.Boolean,
|
|
382
|
+
l: FlagArity.Value,
|
|
383
|
+
selector: FlagArity.Value,
|
|
384
|
+
labels: FlagArity.Value,
|
|
385
|
+
"field-selector": FlagArity.Value,
|
|
386
|
+
|
|
387
|
+
// Streaming: booleans, but allowed only as "=false" (see STREAMING_FLAGS).
|
|
388
|
+
w: FlagArity.Boolean,
|
|
389
|
+
watch: FlagArity.Boolean,
|
|
390
|
+
"watch-only": FlagArity.Boolean,
|
|
391
|
+
follow: FlagArity.Boolean,
|
|
392
|
+
|
|
393
|
+
// logs.
|
|
394
|
+
c: FlagArity.Value,
|
|
395
|
+
container: FlagArity.Value,
|
|
396
|
+
since: FlagArity.Value,
|
|
397
|
+
"since-time": FlagArity.Value,
|
|
398
|
+
tail: FlagArity.Value,
|
|
399
|
+
"limit-bytes": FlagArity.Value,
|
|
400
|
+
"max-log-requests": FlagArity.Value,
|
|
401
|
+
"pod-running-timeout": FlagArity.Value,
|
|
402
|
+
previous: FlagArity.Boolean,
|
|
403
|
+
timestamps: FlagArity.Boolean,
|
|
404
|
+
"all-containers": FlagArity.Boolean,
|
|
405
|
+
"all-pods": FlagArity.Boolean,
|
|
406
|
+
"ignore-errors": FlagArity.Boolean,
|
|
407
|
+
"insecure-skip-tls-verify-backend": FlagArity.Boolean,
|
|
408
|
+
|
|
409
|
+
/*
|
|
410
|
+
* Three names mean different things on different verbs; the value form is
|
|
411
|
+
* the default and VERB_FLAG_OVERRIDES holds the exceptions:
|
|
412
|
+
* -p is --patch (value) on patch, --previous (boolean) on logs
|
|
413
|
+
* --prefix is a value on set env, a boolean on logs
|
|
414
|
+
* --containers is -c (value) on set env/resources, a boolean on top pod
|
|
415
|
+
*/
|
|
416
|
+
p: FlagArity.Value,
|
|
417
|
+
patch: FlagArity.Value,
|
|
418
|
+
prefix: FlagArity.Value,
|
|
419
|
+
containers: FlagArity.Value,
|
|
420
|
+
|
|
421
|
+
// describe, top, events, api-resources, version, explain, auth.
|
|
422
|
+
"show-events": FlagArity.Boolean,
|
|
423
|
+
sum: FlagArity.Boolean,
|
|
424
|
+
"use-protocol-buffers": FlagArity.Boolean,
|
|
425
|
+
"show-capacity": FlagArity.Boolean,
|
|
426
|
+
for: FlagArity.Value,
|
|
427
|
+
types: FlagArity.Value,
|
|
428
|
+
"api-group": FlagArity.Value,
|
|
429
|
+
categories: FlagArity.Value,
|
|
430
|
+
verbs: FlagArity.Value,
|
|
431
|
+
cached: FlagArity.Boolean,
|
|
432
|
+
namespaced: FlagArity.Boolean,
|
|
433
|
+
client: FlagArity.Boolean,
|
|
434
|
+
"api-version": FlagArity.Value,
|
|
435
|
+
list: FlagArity.Boolean,
|
|
436
|
+
q: FlagArity.Boolean,
|
|
437
|
+
quiet: FlagArity.Boolean,
|
|
438
|
+
|
|
439
|
+
// rollout, scale, delete, drain, cordon, label, annotate, taint, patch, set.
|
|
440
|
+
revision: FlagArity.Value,
|
|
441
|
+
"to-revision": FlagArity.Value,
|
|
442
|
+
timeout: FlagArity.Value,
|
|
443
|
+
"field-manager": FlagArity.Value,
|
|
444
|
+
replicas: FlagArity.Value,
|
|
445
|
+
r: FlagArity.Value,
|
|
446
|
+
"current-replicas": FlagArity.Value,
|
|
447
|
+
"resource-version": FlagArity.Value,
|
|
448
|
+
"grace-period": FlagArity.Value,
|
|
449
|
+
"pod-selector": FlagArity.Value,
|
|
450
|
+
"skip-wait-for-delete-timeout": FlagArity.Value,
|
|
451
|
+
force: FlagArity.Boolean,
|
|
452
|
+
now: FlagArity.Boolean,
|
|
453
|
+
wait: FlagArity.Boolean,
|
|
454
|
+
"ignore-daemonsets": FlagArity.Boolean,
|
|
455
|
+
"delete-emptydir-data": FlagArity.Boolean,
|
|
456
|
+
"delete-local-data": FlagArity.Boolean,
|
|
457
|
+
"disable-eviction": FlagArity.Boolean,
|
|
458
|
+
overwrite: FlagArity.Boolean,
|
|
459
|
+
local: FlagArity.Boolean,
|
|
460
|
+
record: FlagArity.Boolean,
|
|
461
|
+
"save-config": FlagArity.Boolean,
|
|
462
|
+
cascade: FlagArity.OptionalValue,
|
|
463
|
+
"dry-run": FlagArity.OptionalValue,
|
|
464
|
+
validate: FlagArity.OptionalValue,
|
|
465
|
+
type: FlagArity.Value,
|
|
466
|
+
image: FlagArity.Value,
|
|
467
|
+
env: FlagArity.Value,
|
|
468
|
+
e: FlagArity.Value,
|
|
469
|
+
from: FlagArity.Value,
|
|
470
|
+
keys: FlagArity.Value,
|
|
471
|
+
limits: FlagArity.Value,
|
|
472
|
+
requests: FlagArity.Value,
|
|
473
|
+
|
|
474
|
+
// create, expose, autoscale.
|
|
475
|
+
restart: FlagArity.Value,
|
|
476
|
+
schedule: FlagArity.Value,
|
|
477
|
+
port: FlagArity.Value,
|
|
478
|
+
"target-port": FlagArity.Value,
|
|
479
|
+
protocol: FlagArity.Value,
|
|
480
|
+
name: FlagArity.Value,
|
|
481
|
+
"external-ip": FlagArity.Value,
|
|
482
|
+
"cluster-ip": FlagArity.Value,
|
|
483
|
+
"load-balancer-ip": FlagArity.Value,
|
|
484
|
+
// Listed for their arity only: every write refuses them (OBJECT_OVERRIDE_FLAGS).
|
|
485
|
+
overrides: FlagArity.Value,
|
|
486
|
+
"override-type": FlagArity.Value,
|
|
487
|
+
"session-affinity": FlagArity.Value,
|
|
488
|
+
min: FlagArity.Value,
|
|
489
|
+
max: FlagArity.Value,
|
|
490
|
+
cpu: FlagArity.Value,
|
|
491
|
+
"cpu-percent": FlagArity.Value,
|
|
492
|
+
"from-literal": FlagArity.Value,
|
|
493
|
+
"append-hash": FlagArity.Boolean,
|
|
494
|
+
resource: FlagArity.Value,
|
|
495
|
+
"resource-name": FlagArity.Value,
|
|
496
|
+
verb: FlagArity.Value,
|
|
497
|
+
"non-resource-url": FlagArity.Value,
|
|
498
|
+
"aggregation-rule": FlagArity.Value,
|
|
499
|
+
role: FlagArity.Value,
|
|
500
|
+
clusterrole: FlagArity.Value,
|
|
501
|
+
group: FlagArity.Value,
|
|
502
|
+
serviceaccount: FlagArity.Value,
|
|
503
|
+
hard: FlagArity.Value,
|
|
504
|
+
scopes: FlagArity.Value,
|
|
505
|
+
"min-available": FlagArity.Value,
|
|
506
|
+
"max-unavailable": FlagArity.Value,
|
|
507
|
+
value: FlagArity.Value,
|
|
508
|
+
"global-default": FlagArity.Boolean,
|
|
509
|
+
description: FlagArity.Value,
|
|
510
|
+
"preemption-policy": FlagArity.Value,
|
|
511
|
+
class: FlagArity.Value,
|
|
512
|
+
rule: FlagArity.Value,
|
|
513
|
+
annotation: FlagArity.Value,
|
|
514
|
+
"default-backend": FlagArity.Value,
|
|
515
|
+
tcp: FlagArity.Value,
|
|
516
|
+
"node-port": FlagArity.Value,
|
|
517
|
+
clusterip: FlagArity.Value,
|
|
518
|
+
"docker-server": FlagArity.Value,
|
|
519
|
+
"docker-username": FlagArity.Value,
|
|
520
|
+
"docker-password": FlagArity.Value,
|
|
521
|
+
"docker-email": FlagArity.Value,
|
|
522
|
+
"windows-line-endings": FlagArity.Boolean,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const KNOWN_FLAG_ARITY: Map<string, FlagArity> = new Map<string, FlagArity>(
|
|
526
|
+
Object.entries(KNOWN_FLAGS),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
/*
|
|
530
|
+
* Flags whose meaning depends on the verb (see the note in KNOWN_FLAGS), and
|
|
531
|
+
* one that is Denied everywhere except on the verb where it is harmless
|
|
532
|
+
* (explain --recursive prints every field; -R/--recursive elsewhere walks a
|
|
533
|
+
* directory of manifests). The verb is the first positional; kubectl itself
|
|
534
|
+
* rejects a subcommand's boolean flag placed before the verb, so these can
|
|
535
|
+
* only ever follow it.
|
|
536
|
+
*/
|
|
537
|
+
const VERB_FLAG_OVERRIDES: Record<string, Record<string, FlagArity>> = {
|
|
538
|
+
logs: { p: FlagArity.Boolean, prefix: FlagArity.Boolean },
|
|
539
|
+
top: { containers: FlagArity.Boolean },
|
|
540
|
+
explain: { recursive: FlagArity.Boolean },
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const VERB_FLAG_OVERRIDE_ARITY: Map<string, Map<string, FlagArity>> = new Map<
|
|
544
|
+
string,
|
|
545
|
+
Map<string, FlagArity>
|
|
546
|
+
>(
|
|
547
|
+
Object.entries(VERB_FLAG_OVERRIDES).map(
|
|
548
|
+
([verb, overrides]: [string, Record<string, FlagArity>]): [
|
|
549
|
+
string,
|
|
550
|
+
Map<string, FlagArity>,
|
|
551
|
+
] => {
|
|
552
|
+
return [verb, new Map<string, FlagArity>(Object.entries(overrides))];
|
|
553
|
+
},
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
/*
|
|
558
|
+
* kubectl's global flags that OneUptime AI may use. They are the only flags
|
|
559
|
+
* allowed before the verb (see the Flags section): kubectl parses them at
|
|
560
|
+
* the root, so writing them first cannot shift which command runs.
|
|
561
|
+
*/
|
|
562
|
+
const GLOBAL_FLAGS: Set<string> = new Set<string>([
|
|
563
|
+
"n",
|
|
564
|
+
"namespace",
|
|
565
|
+
"request-timeout",
|
|
566
|
+
"match-server-version",
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
/*
|
|
570
|
+
* Verbs whose next positional is a subcommand (rollout status, set image,
|
|
571
|
+
* create job, auth can-i, cluster-info dump, top pod). The ordering rule
|
|
572
|
+
* applies between the verb and the subcommand as well.
|
|
573
|
+
*/
|
|
574
|
+
const SUBCOMMAND_VERBS: Set<string> = new Set<string>([
|
|
575
|
+
"rollout",
|
|
576
|
+
"set",
|
|
577
|
+
"create",
|
|
578
|
+
"auth",
|
|
579
|
+
"cluster-info",
|
|
580
|
+
"top",
|
|
581
|
+
]);
|
|
582
|
+
|
|
583
|
+
/*
|
|
584
|
+
* Flags that are Denied wherever they appear, with why. Short and long
|
|
585
|
+
* spellings are both listed because a short flag can hide inside a cluster
|
|
586
|
+
* ("-As", "-pf") and is found letter by letter.
|
|
587
|
+
*
|
|
588
|
+
* kubectl v1.36's global flags are --as --as-group --as-uid --as-user-extra
|
|
589
|
+
* --cache-dir --certificate-authority --client-certificate --client-key
|
|
590
|
+
* --cluster --context --disable-compression --insecure-skip-tls-verify
|
|
591
|
+
* --kubeconfig --kuberc --log-flush-frequency --match-server-version
|
|
592
|
+
* -n/--namespace --password --profile --profile-output --request-timeout
|
|
593
|
+
* -s/--server --tls-server-name --token --user --username -v/--v --vmodule
|
|
594
|
+
* --warnings-as-errors. Every one of them except GLOBAL_FLAGS is listed
|
|
595
|
+
* here: each selects credentials, an identity or a cluster, writes a file
|
|
596
|
+
* on the Runner, or logs request bodies. (An unlisted flag is refused
|
|
597
|
+
* anyway; listing them names the reason.)
|
|
598
|
+
*/
|
|
599
|
+
const DENIED_FLAG_GROUPS: Array<{ reason: string; flags: Array<string> }> = [
|
|
600
|
+
{
|
|
601
|
+
reason:
|
|
602
|
+
"OneUptime AI may only use the cluster access it was given, never other credentials, another identity or another cluster",
|
|
603
|
+
flags: [
|
|
604
|
+
"kubeconfig",
|
|
605
|
+
"kuberc",
|
|
606
|
+
"token",
|
|
607
|
+
"server",
|
|
608
|
+
"s",
|
|
609
|
+
"as",
|
|
610
|
+
"as-group",
|
|
611
|
+
"as-uid",
|
|
612
|
+
"as-user-extra",
|
|
613
|
+
"context",
|
|
614
|
+
"cluster",
|
|
615
|
+
"user",
|
|
616
|
+
"username",
|
|
617
|
+
"password",
|
|
618
|
+
"client-certificate",
|
|
619
|
+
"client-key",
|
|
620
|
+
"certificate-authority",
|
|
621
|
+
"insecure-skip-tls-verify",
|
|
622
|
+
"tls-server-name",
|
|
623
|
+
],
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
reason:
|
|
627
|
+
"it makes kubectl read a file on the Runner (write templates and patches inline instead)",
|
|
628
|
+
flags: [
|
|
629
|
+
"filename",
|
|
630
|
+
"f",
|
|
631
|
+
"kustomize",
|
|
632
|
+
"k",
|
|
633
|
+
"recursive",
|
|
634
|
+
"R",
|
|
635
|
+
"patch-file",
|
|
636
|
+
"from-file",
|
|
637
|
+
"from-env-file",
|
|
638
|
+
"cert",
|
|
639
|
+
"key",
|
|
640
|
+
],
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
reason:
|
|
644
|
+
"it names the template that -o go-template-file reads from a file on the Runner (write it inline as -o go-template=... or -o jsonpath=... instead)",
|
|
645
|
+
flags: ["template"],
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
reason: "it turns kubectl into a raw API client",
|
|
649
|
+
flags: ["raw"],
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
reason:
|
|
653
|
+
"it resolves Secret and ConfigMap references into their plaintext values (kubectl set env --list --resolve prints them)",
|
|
654
|
+
flags: ["resolve"],
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
reason:
|
|
658
|
+
"kubectl's own logging, profiling and cache flags write on the Runner or print request bodies and credentials",
|
|
659
|
+
flags: [
|
|
660
|
+
"v",
|
|
661
|
+
"vmodule",
|
|
662
|
+
"log-file",
|
|
663
|
+
"log-dir",
|
|
664
|
+
"log-file-max-size",
|
|
665
|
+
"log-backtrace-at",
|
|
666
|
+
"log-flush-frequency",
|
|
667
|
+
"alsologtostderr",
|
|
668
|
+
"logtostderr",
|
|
669
|
+
"add-dir-header",
|
|
670
|
+
"one-output",
|
|
671
|
+
"skip-headers",
|
|
672
|
+
"skip-log-headers",
|
|
673
|
+
"stderrthreshold",
|
|
674
|
+
"profile",
|
|
675
|
+
"profile-output",
|
|
676
|
+
"cache-dir",
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
reason: "it needs a terminal or an editor",
|
|
681
|
+
flags: ["i", "interactive", "t", "tty", "stdin", "edit"],
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
reason:
|
|
685
|
+
"it has nothing to offer an SRE agent (kubectl explain documents resources)",
|
|
686
|
+
flags: ["h", "help", "warnings-as-errors", "disable-compression"],
|
|
687
|
+
},
|
|
688
|
+
];
|
|
689
|
+
|
|
690
|
+
const DENIED_FLAG_REASONS: Map<string, string> = new Map<string, string>();
|
|
691
|
+
|
|
692
|
+
for (const group of DENIED_FLAG_GROUPS) {
|
|
693
|
+
for (const flag of group.flags) {
|
|
694
|
+
DENIED_FLAG_REASONS.set(flag, group.reason);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/*
|
|
699
|
+
* Flags that keep kubectl running until it is killed. Boolean, so the only
|
|
700
|
+
* way to write them off is "=false", which is what the model should write
|
|
701
|
+
* on rollout status (which watches by default).
|
|
702
|
+
*/
|
|
703
|
+
const STREAMING_FLAGS: Set<string> = new Set<string>([
|
|
704
|
+
"w",
|
|
705
|
+
"watch",
|
|
706
|
+
"watch-only",
|
|
707
|
+
"follow",
|
|
708
|
+
]);
|
|
709
|
+
|
|
710
|
+
/*
|
|
711
|
+
* Flags that replace the object kubectl generates with whatever their JSON
|
|
712
|
+
* describes (kubectl expose; see the header's Denied list). They stay in
|
|
713
|
+
* KNOWN_FLAGS with their real arity, so `--overrides '{...}'` is still split
|
|
714
|
+
* where kubectl splits it and the verb is still found, and every write that
|
|
715
|
+
* carries one is refused in evaluateArgs — in every spelling, because
|
|
716
|
+
* parseArgs files `--override_type` and `--overrides=x` under these names —
|
|
717
|
+
* as is an allowlist entry that spells one (readAllowlistEntry). A read
|
|
718
|
+
* keeps them: no read verb takes them (kubectl refuses the command) and a
|
|
719
|
+
* read creates nothing.
|
|
720
|
+
*/
|
|
721
|
+
const OBJECT_OVERRIDE_FLAGS: Array<string> = ["overrides", "override-type"];
|
|
722
|
+
|
|
723
|
+
/*
|
|
724
|
+
* ---- Output formats --------------------------------------------------------
|
|
725
|
+
*
|
|
726
|
+
* -o/--output values, compared lowercased. Formats that take an argument
|
|
727
|
+
* must carry it inline after "=": -o jsonpath={.items[*].name}. Written bare
|
|
728
|
+
* they take it from --template, which is Denied, so the bare form is refused
|
|
729
|
+
* too. The *-file formats make kubectl read a path on the Runner and echo it
|
|
730
|
+
* — a Read-tier `get` would print the pod's ServiceAccount token — so they
|
|
731
|
+
* are Denied in every spelling (-o X=..., -oX=..., --output X=...,
|
|
732
|
+
* --output=X=...); anything not listed is refused rather than guessed.
|
|
733
|
+
*/
|
|
734
|
+
const OUTPUT_FORMATS: Set<string> = new Set<string>([
|
|
735
|
+
"json",
|
|
736
|
+
"yaml",
|
|
737
|
+
"name",
|
|
738
|
+
"wide",
|
|
739
|
+
"plaintext",
|
|
740
|
+
"plaintext-openapiv2",
|
|
741
|
+
]);
|
|
742
|
+
|
|
743
|
+
const OUTPUT_FORMATS_WITH_ARGUMENT: Set<string> = new Set<string>([
|
|
744
|
+
"jsonpath",
|
|
745
|
+
"jsonpath-as-json",
|
|
746
|
+
"go-template",
|
|
747
|
+
"template",
|
|
748
|
+
"custom-columns",
|
|
749
|
+
]);
|
|
750
|
+
|
|
751
|
+
const FILE_OUTPUT_FORMATS: Set<string> = new Set<string>([
|
|
752
|
+
"go-template-file",
|
|
753
|
+
"templatefile",
|
|
754
|
+
"jsonpath-file",
|
|
755
|
+
"custom-columns-file",
|
|
756
|
+
]);
|
|
757
|
+
|
|
758
|
+
// ---- Verbs -------------------------------------------------------------------
|
|
759
|
+
|
|
760
|
+
const READ_VERBS: Set<string> = new Set<string>([
|
|
761
|
+
"get",
|
|
762
|
+
"describe",
|
|
763
|
+
"logs",
|
|
764
|
+
"top",
|
|
765
|
+
"events",
|
|
766
|
+
"api-resources",
|
|
767
|
+
"api-versions",
|
|
768
|
+
"version",
|
|
769
|
+
"explain",
|
|
770
|
+
]);
|
|
771
|
+
|
|
772
|
+
/*
|
|
773
|
+
* The write verbs that CAN be SafeWrite — rollout restart/undo/pause/resume,
|
|
774
|
+
* delete, scale, cordon, uncordon, label and annotate — each have their own
|
|
775
|
+
* branch in evaluateArgs, because each has its own "exactly one named
|
|
776
|
+
* object" shape. These are always at least RiskyWrite:
|
|
777
|
+
*/
|
|
778
|
+
const RISKY_WRITE_VERBS: Set<string> = new Set<string>([
|
|
779
|
+
"patch",
|
|
780
|
+
"set",
|
|
781
|
+
"taint",
|
|
782
|
+
"drain",
|
|
783
|
+
"create",
|
|
784
|
+
"expose",
|
|
785
|
+
"autoscale",
|
|
786
|
+
]);
|
|
787
|
+
|
|
788
|
+
/*
|
|
789
|
+
* Verbs whose operands are node NAMES (or TYPE/NAME), and nodes live outside
|
|
790
|
+
* any namespace. taint acts on nodes too, but names its kind like any object
|
|
791
|
+
* verb ("taint nodes NAME ..."), so the kind rule reads it.
|
|
792
|
+
*/
|
|
793
|
+
const NODE_NAME_VERBS: Set<string> = new Set<string>([
|
|
794
|
+
"cordon",
|
|
795
|
+
"uncordon",
|
|
796
|
+
"drain",
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
/*
|
|
800
|
+
* Node verbs that move pods off a node in every namespace (a drain evicts
|
|
801
|
+
* them; a NoExecute taint makes the node evict them, and any taint decides
|
|
802
|
+
* which pods it keeps), with what the approval card says about each. They
|
|
803
|
+
* carry `requiresHuman`: nothing an operator sets on the cluster runs them
|
|
804
|
+
* unattended (see the header). A patch of a Node carries it too
|
|
805
|
+
* (NODE_PATCH_ALWAYS_HUMAN_REASON): a taint is only the Node's spec.taints.
|
|
806
|
+
* describeAlwaysHumanWrite() is the one place that reads these.
|
|
807
|
+
*/
|
|
808
|
+
const ALWAYS_HUMAN_VERB_REASONS: Record<string, string> = {
|
|
809
|
+
drain:
|
|
810
|
+
"kubectl drain evicts every pod on the node, in every namespace (kube-system and the OneUptime agent's own included), so a human always approves it",
|
|
811
|
+
taint:
|
|
812
|
+
"kubectl taint decides which pods a node keeps, in every namespace (kube-system and the OneUptime agent's own included; a NoExecute taint evicts the rest at once), so a human always approves it",
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const NODE_PATCH_ALWAYS_HUMAN_REASON: string =
|
|
816
|
+
"kubectl patch of a Node can set, replace or clear its taints, which decide which pods the node keeps, in every namespace (kube-system and the OneUptime agent's own included; a NoExecute taint evicts the rest at once), so a human always approves it, whatever the patch body says (kubectl cordon and uncordon change scheduling on one node without one)";
|
|
817
|
+
|
|
818
|
+
/*
|
|
819
|
+
* The write verbs evaluateArgs tiers in a branch of their own (the ones that
|
|
820
|
+
* can be SafeWrite). With READ_VERBS, RISKY_WRITE_VERBS and the two
|
|
821
|
+
* read-only command groups below, these are every verb OneUptime AI may run;
|
|
822
|
+
* evaluateArgs refuses any other word as an unknown verb.
|
|
823
|
+
*/
|
|
824
|
+
const OWN_BRANCH_WRITE_VERBS: Set<string> = new Set<string>([
|
|
825
|
+
"rollout",
|
|
826
|
+
"delete",
|
|
827
|
+
"scale",
|
|
828
|
+
"cordon",
|
|
829
|
+
"uncordon",
|
|
830
|
+
"label",
|
|
831
|
+
"annotate",
|
|
832
|
+
]);
|
|
833
|
+
|
|
834
|
+
const READ_COMMAND_GROUPS: Set<string> = new Set<string>([
|
|
835
|
+
"cluster-info",
|
|
836
|
+
"auth",
|
|
837
|
+
]);
|
|
838
|
+
|
|
839
|
+
// Verbs whose subcommand must be written out for the command to run at all.
|
|
840
|
+
const SUBCOMMAND_REQUIRED_VERBS: Set<string> = new Set<string>([
|
|
841
|
+
"rollout",
|
|
842
|
+
"set",
|
|
843
|
+
"create",
|
|
844
|
+
]);
|
|
845
|
+
|
|
846
|
+
function isVerbOneUptimeAiMayRun(verb: string): boolean {
|
|
847
|
+
return (
|
|
848
|
+
READ_VERBS.has(verb) ||
|
|
849
|
+
RISKY_WRITE_VERBS.has(verb) ||
|
|
850
|
+
OWN_BRANCH_WRITE_VERBS.has(verb) ||
|
|
851
|
+
READ_COMMAND_GROUPS.has(verb)
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/*
|
|
856
|
+
* Verbs with nothing to offer an SRE agent, or that open an interactive or
|
|
857
|
+
* long-lived channel, or that could rewrite objects from input we do not
|
|
858
|
+
* support. Listed explicitly so the reason names the verb; any verb not in
|
|
859
|
+
* any list is Denied too — kubectl runs a `kubectl-<verb>` plugin from the
|
|
860
|
+
* Runner's PATH for a word it does not know (and a kuberc alias for one it
|
|
861
|
+
* has been taught), so an unknown verb is never a guess we can afford.
|
|
862
|
+
*/
|
|
863
|
+
const DENIED_VERBS: Set<string> = new Set<string>([
|
|
864
|
+
"kuberc",
|
|
865
|
+
"exec",
|
|
866
|
+
"attach",
|
|
867
|
+
"cp",
|
|
868
|
+
"port-forward",
|
|
869
|
+
"proxy",
|
|
870
|
+
"debug",
|
|
871
|
+
"run",
|
|
872
|
+
"edit",
|
|
873
|
+
"replace",
|
|
874
|
+
"apply",
|
|
875
|
+
"diff",
|
|
876
|
+
"kustomize",
|
|
877
|
+
"config",
|
|
878
|
+
"certificate",
|
|
879
|
+
"wait",
|
|
880
|
+
"plugin",
|
|
881
|
+
"completion",
|
|
882
|
+
"options",
|
|
883
|
+
"help",
|
|
884
|
+
"alpha",
|
|
885
|
+
"convert",
|
|
886
|
+
]);
|
|
887
|
+
|
|
888
|
+
/*
|
|
889
|
+
* Kinds whose deletion is either unrecoverable or cluster-wide. Deleting
|
|
890
|
+
* them is Denied even with a human in the loop — this lane is for fixing a
|
|
891
|
+
* workload, not for tearing down a cluster. Matched on normalizeKind(), so
|
|
892
|
+
* every spelling kubectl accepts is covered: plural, singular, short name,
|
|
893
|
+
* Kind case and group-qualified (`crd`, `CustomResourceDefinition`,
|
|
894
|
+
* `customresourcedefinitions.apiextensions.k8s.io`, `ns/prod`, ...).
|
|
895
|
+
* Resource CATEGORIES (all, api-extensions) are refused separately, in every
|
|
896
|
+
* write verb (see RESOURCE_CATEGORIES).
|
|
897
|
+
*/
|
|
898
|
+
const NEVER_DELETE_KINDS: Set<string> = new Set<string>([
|
|
899
|
+
"namespace",
|
|
900
|
+
"node",
|
|
901
|
+
"persistentvolume",
|
|
902
|
+
"persistentvolumeclaim",
|
|
903
|
+
"customresourcedefinition",
|
|
904
|
+
"clusterrole",
|
|
905
|
+
"clusterrolebinding",
|
|
906
|
+
"role",
|
|
907
|
+
"rolebinding",
|
|
908
|
+
"storageclass",
|
|
909
|
+
"secret",
|
|
910
|
+
"priorityclass",
|
|
911
|
+
"apiservice",
|
|
912
|
+
"mutatingwebhookconfiguration",
|
|
913
|
+
"validatingwebhookconfiguration",
|
|
914
|
+
"validatingadmissionpolicy",
|
|
915
|
+
"validatingadmissionpolicybinding",
|
|
916
|
+
"mutatingadmissionpolicy",
|
|
917
|
+
"mutatingadmissionpolicybinding",
|
|
918
|
+
"certificatesigningrequest",
|
|
919
|
+
]);
|
|
920
|
+
|
|
921
|
+
/*
|
|
922
|
+
* Resource categories. kubectl expands a category into every kind that
|
|
923
|
+
* declares it (ReplaceAliases, through discovery) BEFORE it resolves types,
|
|
924
|
+
* so `kubectl delete api-extensions -l x` deletes CRDs, APIServices and
|
|
925
|
+
* admission webhooks although none of those kinds appears in the command.
|
|
926
|
+
* A category is therefore never allowed in a write, whatever the verb.
|
|
927
|
+
* `all` and `api-extensions` are the categories the API server itself
|
|
928
|
+
* defines (the only ones that can reach built-in kinds); the rest are the
|
|
929
|
+
* categories widely used operators give their custom resources — a custom
|
|
930
|
+
* category can only reach custom resources, which are RiskyWrite anyway, so
|
|
931
|
+
* this list is a clearer refusal, not the only line. Reads (`get all`)
|
|
932
|
+
* stay Read.
|
|
933
|
+
*/
|
|
934
|
+
const RESOURCE_CATEGORIES: Set<string> = new Set<string>([
|
|
935
|
+
"all",
|
|
936
|
+
"api-extensions",
|
|
937
|
+
"crossplane",
|
|
938
|
+
"managed",
|
|
939
|
+
"composite",
|
|
940
|
+
"claim",
|
|
941
|
+
"knative",
|
|
942
|
+
"serving",
|
|
943
|
+
"eventing",
|
|
944
|
+
"istio-io",
|
|
945
|
+
"networking-istio-io",
|
|
946
|
+
"security-istio-io",
|
|
947
|
+
"telemetry-istio-io",
|
|
948
|
+
"extensions-istio-io",
|
|
949
|
+
"gateway-api",
|
|
950
|
+
"cert-manager",
|
|
951
|
+
"kyverno",
|
|
952
|
+
"cluster-api",
|
|
953
|
+
"tekton",
|
|
954
|
+
"tekton-pipelines",
|
|
955
|
+
]);
|
|
956
|
+
|
|
957
|
+
/*
|
|
958
|
+
* Kinds that decide who may do what. OneUptime AI never creates or changes
|
|
959
|
+
* them in any write verb — label and annotate included (a label on a
|
|
960
|
+
* ClusterRole can aggregate its rules into admin/edit/view) — because a new
|
|
961
|
+
* or widened binding is a privilege grant, not a fix; nor does it delete
|
|
962
|
+
* them (NEVER_DELETE_KINDS). Reading them stays Read: an investigation needs
|
|
963
|
+
* to see why something is Forbidden.
|
|
964
|
+
*/
|
|
965
|
+
const RBAC_KINDS: Set<string> = new Set<string>([
|
|
966
|
+
"role",
|
|
967
|
+
"clusterrole",
|
|
968
|
+
"rolebinding",
|
|
969
|
+
"clusterrolebinding",
|
|
970
|
+
]);
|
|
971
|
+
|
|
972
|
+
/*
|
|
973
|
+
* Kinds that decide, for the whole cluster, what the API server admits and
|
|
974
|
+
* serves: admission webhooks and admission policies (and their bindings)
|
|
975
|
+
* accept, reject or rewrite every object written; an APIService routes a
|
|
976
|
+
* whole API group to a Service; a CRD defines (and converts) a whole kind.
|
|
977
|
+
* Emptying a webhook's rules, setting a policy binding to Audit or
|
|
978
|
+
* repointing an APIService switches a cluster control off as surely as
|
|
979
|
+
* deleting it (which NEVER_DELETE_KINDS already refuses) — the same reason
|
|
980
|
+
* Pod Security Admission labels are Denied. So every write verb on these is
|
|
981
|
+
* Denied; reading them stays Read.
|
|
982
|
+
*/
|
|
983
|
+
const ADMISSION_AND_API_EXTENSION_KINDS: Set<string> = new Set<string>([
|
|
984
|
+
"mutatingwebhookconfiguration",
|
|
985
|
+
"validatingwebhookconfiguration",
|
|
986
|
+
"validatingadmissionpolicy",
|
|
987
|
+
"validatingadmissionpolicybinding",
|
|
988
|
+
"mutatingadmissionpolicy",
|
|
989
|
+
"mutatingadmissionpolicybinding",
|
|
990
|
+
"apiservice",
|
|
991
|
+
"customresourcedefinition",
|
|
992
|
+
]);
|
|
993
|
+
|
|
994
|
+
/*
|
|
995
|
+
* Kinds whose objects ARE credentials. OneUptime AI never reads, changes or
|
|
996
|
+
* deletes them, in any spelling kubectl accepts (secret, secrets, Secret,
|
|
997
|
+
* secrets.v1., secret/name, pods,secrets, ...): get/describe would print
|
|
998
|
+
* their values to the model, and label/annotate/patch with -o yaml print the
|
|
999
|
+
* whole object too, so the rule is per kind, not per verb. `all` does not
|
|
1000
|
+
* include secrets, so `get all` stays Read. Pods and ConfigMaps can carry
|
|
1001
|
+
* plaintext env values as well; those cannot be denied without blinding the
|
|
1002
|
+
* investigation, so output redaction covers them, not this policy.
|
|
1003
|
+
*/
|
|
1004
|
+
const CREDENTIAL_KINDS: Set<string> = new Set<string>(["secret"]);
|
|
1005
|
+
|
|
1006
|
+
/*
|
|
1007
|
+
* Verbs whose positionals name objects as "kind name..." or "kind/name...",
|
|
1008
|
+
* where CREDENTIAL_KINDS is enforced. For rollout and set the objects follow
|
|
1009
|
+
* the subcommand. Node verbs (cordon, drain) name nodes, logs names pods and
|
|
1010
|
+
* the remaining verbs take no objects, so the kind cannot appear there.
|
|
1011
|
+
*/
|
|
1012
|
+
const OBJECT_VERBS: Set<string> = new Set<string>([
|
|
1013
|
+
"get",
|
|
1014
|
+
"describe",
|
|
1015
|
+
"delete",
|
|
1016
|
+
"label",
|
|
1017
|
+
"annotate",
|
|
1018
|
+
"patch",
|
|
1019
|
+
"scale",
|
|
1020
|
+
"autoscale",
|
|
1021
|
+
"expose",
|
|
1022
|
+
"taint",
|
|
1023
|
+
"rollout",
|
|
1024
|
+
"set",
|
|
1025
|
+
]);
|
|
1026
|
+
|
|
1027
|
+
/*
|
|
1028
|
+
* Kinds whose deletion is a safe change: exactly one, and only the pod. A
|
|
1029
|
+
* pod with a controller (ReplicaSet, StatefulSet, DaemonSet, Job) is
|
|
1030
|
+
* recreated by it. A Job is NOT recreated by anything — a standalone Job (a
|
|
1031
|
+
* migration, a one-off batch run) is simply gone, and a CronJob only starts
|
|
1032
|
+
* a new one on its next schedule — so deleting a Job is RiskyWrite.
|
|
1033
|
+
* Matched on builtinKind(), so a custom resource that happens to be called
|
|
1034
|
+
* `pods.example.com` is not a pod.
|
|
1035
|
+
*/
|
|
1036
|
+
const SAFE_DELETE_KINDS: Set<string> = new Set<string>(["pod"]);
|
|
1037
|
+
|
|
1038
|
+
// The workloads `kubectl rollout restart/undo/pause/resume` acts on.
|
|
1039
|
+
const ROLLOUT_SAFE_KINDS: Set<string> = new Set<string>([
|
|
1040
|
+
"deployment",
|
|
1041
|
+
"statefulset",
|
|
1042
|
+
"daemonset",
|
|
1043
|
+
]);
|
|
1044
|
+
|
|
1045
|
+
/*
|
|
1046
|
+
* The workloads a safe `kubectl scale` may target — the ones whose scale
|
|
1047
|
+
* subresource the kubernetes-agent chart's remediation RBAC grants.
|
|
1048
|
+
*/
|
|
1049
|
+
const SCALE_SAFE_KINDS: Set<string> = new Set<string>([
|
|
1050
|
+
"deployment",
|
|
1051
|
+
"statefulset",
|
|
1052
|
+
"replicaset",
|
|
1053
|
+
]);
|
|
1054
|
+
|
|
1055
|
+
/*
|
|
1056
|
+
* The kinds a safe label/annotate may target: a pod or a workload. A label
|
|
1057
|
+
* on these is metadata a fix may need (quarantine a pod by dropping its
|
|
1058
|
+
* `app` label, note a restart). On any other kind a label is a control:
|
|
1059
|
+
* Pod Security Admission reads namespace labels, DaemonSet nodeSelectors and
|
|
1060
|
+
* cloud load balancers read node labels, Services and NetworkPolicies select
|
|
1061
|
+
* by label, ClusterRole aggregation reads role labels, and ServiceAccount /
|
|
1062
|
+
* Ingress annotations bind cloud identities and inject proxy config.
|
|
1063
|
+
*/
|
|
1064
|
+
const LABEL_SAFE_KINDS: Set<string> = new Set<string>([
|
|
1065
|
+
"pod",
|
|
1066
|
+
"deployment",
|
|
1067
|
+
"statefulset",
|
|
1068
|
+
"daemonset",
|
|
1069
|
+
"replicaset",
|
|
1070
|
+
"job",
|
|
1071
|
+
"cronjob",
|
|
1072
|
+
]);
|
|
1073
|
+
|
|
1074
|
+
/*
|
|
1075
|
+
* Built-in kinds that live outside any namespace, and the API group each
|
|
1076
|
+
* lives in. A write that names only these is not "in" the namespace its -n
|
|
1077
|
+
* flag names (kubectl ignores -n for them), so the protected-namespace rule
|
|
1078
|
+
* looks at the object instead. Matched on builtinKind(), never on
|
|
1079
|
+
* normalizeKind(): `nodes.example.com` or `namespaces.example.com` is some
|
|
1080
|
+
* custom resource that happens to borrow the name. Its scope is unknown
|
|
1081
|
+
* here, so it is judged by the namespace -n names like any namespaced
|
|
1082
|
+
* object — the reading that can only add a human, never remove one.
|
|
1083
|
+
*/
|
|
1084
|
+
const CLUSTER_SCOPED_KIND_GROUPS: Record<string, string> = {
|
|
1085
|
+
node: "",
|
|
1086
|
+
namespace: "",
|
|
1087
|
+
persistentvolume: "",
|
|
1088
|
+
customresourcedefinition: "apiextensions.k8s.io",
|
|
1089
|
+
clusterrole: "rbac.authorization.k8s.io",
|
|
1090
|
+
clusterrolebinding: "rbac.authorization.k8s.io",
|
|
1091
|
+
storageclass: "storage.k8s.io",
|
|
1092
|
+
priorityclass: "scheduling.k8s.io",
|
|
1093
|
+
apiservice: "apiregistration.k8s.io",
|
|
1094
|
+
mutatingwebhookconfiguration: "admissionregistration.k8s.io",
|
|
1095
|
+
validatingwebhookconfiguration: "admissionregistration.k8s.io",
|
|
1096
|
+
validatingadmissionpolicy: "admissionregistration.k8s.io",
|
|
1097
|
+
validatingadmissionpolicybinding: "admissionregistration.k8s.io",
|
|
1098
|
+
mutatingadmissionpolicy: "admissionregistration.k8s.io",
|
|
1099
|
+
mutatingadmissionpolicybinding: "admissionregistration.k8s.io",
|
|
1100
|
+
certificatesigningrequest: "certificates.k8s.io",
|
|
1101
|
+
ingressclass: "networking.k8s.io",
|
|
1102
|
+
runtimeclass: "node.k8s.io",
|
|
1103
|
+
csidriver: "storage.k8s.io",
|
|
1104
|
+
csinode: "storage.k8s.io",
|
|
1105
|
+
volumeattachment: "storage.k8s.io",
|
|
1106
|
+
flowschema: "flowcontrol.apiserver.k8s.io",
|
|
1107
|
+
prioritylevelconfiguration: "flowcontrol.apiserver.k8s.io",
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
const CLUSTER_SCOPED_KINDS: Set<string> = new Set<string>(
|
|
1111
|
+
Object.keys(CLUSTER_SCOPED_KIND_GROUPS),
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
/*
|
|
1115
|
+
* The API group of every built-in kind a SAFE or SCOPE decision names: the
|
|
1116
|
+
* kinds in the *_SAFE_KINDS sets and the cluster-scoped kinds above.
|
|
1117
|
+
* builtinKind() accepts a group-qualified spelling only in this group, so
|
|
1118
|
+
* `deployments.apps/web` is a Deployment and `storageclasses.storage.k8s.io`
|
|
1119
|
+
* a StorageClass, while `jobs.batch.volcano.sh`, `pods.example.com` or
|
|
1120
|
+
* `nodes.example.com` is some custom resource: never a safe target, and
|
|
1121
|
+
* never exempt from the namespace -n names.
|
|
1122
|
+
*/
|
|
1123
|
+
const BUILTIN_KIND_GROUPS: Record<string, string> = {
|
|
1124
|
+
pod: "",
|
|
1125
|
+
deployment: "apps",
|
|
1126
|
+
statefulset: "apps",
|
|
1127
|
+
daemonset: "apps",
|
|
1128
|
+
replicaset: "apps",
|
|
1129
|
+
job: "batch",
|
|
1130
|
+
cronjob: "batch",
|
|
1131
|
+
...CLUSTER_SCOPED_KIND_GROUPS,
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
/*
|
|
1135
|
+
* Label / annotation key prefixes (the DNS part before "/") that belong to
|
|
1136
|
+
* Kubernetes itself — kubernetes.io and k8s.io and every subdomain
|
|
1137
|
+
* (pod-security.kubernetes.io, node-role.kubernetes.io,
|
|
1138
|
+
* rbac.authorization.k8s.io, kubectl.kubernetes.io, ...) — or to the
|
|
1139
|
+
* identity, admission and GitOps controllers that act on them. Writing one
|
|
1140
|
+
* of these steers a controller rather than annotating an object, so it is
|
|
1141
|
+
* RiskyWrite on any kind. Matched as the domain itself or a subdomain.
|
|
1142
|
+
*/
|
|
1143
|
+
const RESERVED_KEY_DOMAINS: Array<string> = [
|
|
1144
|
+
"kubernetes.io",
|
|
1145
|
+
"k8s.io",
|
|
1146
|
+
"gatekeeper.sh",
|
|
1147
|
+
"kyverno.io",
|
|
1148
|
+
"argoproj.io",
|
|
1149
|
+
"eks.amazonaws.com",
|
|
1150
|
+
"iam.gke.io",
|
|
1151
|
+
"azure.workload.identity",
|
|
1152
|
+
];
|
|
1153
|
+
|
|
1154
|
+
/*
|
|
1155
|
+
* Pod Security Admission's namespace labels. Lowering or removing them lets
|
|
1156
|
+
* privileged pods into the namespace, which is never a fix OneUptime AI
|
|
1157
|
+
* makes (Denied, in label, annotate and patch alike).
|
|
1158
|
+
*/
|
|
1159
|
+
const POD_SECURITY_KEY_DOMAIN: string = "pod-security.kubernetes.io";
|
|
1160
|
+
|
|
1161
|
+
/*
|
|
1162
|
+
* The AppArmor profile as an annotation on a pod (or a pod template):
|
|
1163
|
+
* `container.apparmor.security.beta.kubernetes.io/<container>: unconfined`.
|
|
1164
|
+
* It is the deprecated spelling of securityContext.appArmorProfile (the API
|
|
1165
|
+
* server copies it into that field when it creates a pod, for as long as it
|
|
1166
|
+
* supports the annotation) and a Pod Security Standards baseline control of
|
|
1167
|
+
* its own, so a patch body may not name one, like a POD_SECURITY_PATCH_KEYS
|
|
1168
|
+
* field. Matched as this domain or a subdomain of it.
|
|
1169
|
+
*/
|
|
1170
|
+
const APPARMOR_ANNOTATION_KEY_DOMAIN: string =
|
|
1171
|
+
"apparmor.security.beta.kubernetes.io";
|
|
1172
|
+
|
|
1173
|
+
/*
|
|
1174
|
+
* ClusterRole aggregation: a ClusterRole labelled with one of these has its
|
|
1175
|
+
* rules merged into admin / edit / view (and so granted to everyone bound
|
|
1176
|
+
* to them). That is an RBAC grant, Denied like creating a binding.
|
|
1177
|
+
*/
|
|
1178
|
+
const RBAC_AGGREGATION_KEY_PREFIX: string =
|
|
1179
|
+
"rbac.authorization.k8s.io/aggregate-to-";
|
|
1180
|
+
|
|
1181
|
+
/*
|
|
1182
|
+
* Field names a `kubectl patch` body may never touch, matched by key name
|
|
1183
|
+
* (case-insensitively) anywhere in the body — strategic-merge and merge
|
|
1184
|
+
* patches as keys, JSON patches as path segments. Bodies are JSON only (a
|
|
1185
|
+
* body JSON.parse rejects is Denied outright; see findPatchBodyProblem), so
|
|
1186
|
+
* the walker sees exactly the keys kubectl applies. Each one
|
|
1187
|
+
* changes which identity, privileges, host access, Secrets or program a pod
|
|
1188
|
+
* runs with. With write RBAC on a workload that is the same as running any
|
|
1189
|
+
* image as any ServiceAccount in the namespace and reading every Secret it
|
|
1190
|
+
* can mount, which no approval card should be asked to wave through. Image,
|
|
1191
|
+
* env values, resources and replicas stay patchable (set image / set env /
|
|
1192
|
+
* set resources say the same thing more plainly). A patch that replaces or
|
|
1193
|
+
* removes an object ABOVE these fields drops them without naming any; that
|
|
1194
|
+
* is refused by findPodSpecReplacement (see POD_SPEC_PATH_STEPS).
|
|
1195
|
+
*
|
|
1196
|
+
* The host and isolation fields cover every control of the Pod Security
|
|
1197
|
+
* Standards baseline profile (and the restricted profile adds nothing
|
|
1198
|
+
* outside securityContext and volumes): the host namespaces (hostNetwork,
|
|
1199
|
+
* hostPID, hostIPC), hostPath volumes, a container port's hostPort (it binds
|
|
1200
|
+
* the node's own IP), and — under securityContext — privileged,
|
|
1201
|
+
* capabilities, hostProcess, SELinux, AppArmor, seccomp, procMount and
|
|
1202
|
+
* sysctls; plus what weakens a pod's isolation outside those: hostUsers
|
|
1203
|
+
* (true, the default, takes away the user namespace false gives the pod),
|
|
1204
|
+
* runtimeClassName (it can move the pod off a sandboxed runtime) and
|
|
1205
|
+
* shareProcessNamespace. Two baseline controls are refused beside this list,
|
|
1206
|
+
* because a name alone does not say what they are: the AppArmor annotation
|
|
1207
|
+
* (APPARMOR_ANNOTATION_KEY_DOMAIN) and the host of a probe or lifecycle hook
|
|
1208
|
+
* (HOST_FIELD_HANDLER_KEYS; `host` elsewhere is no pod field).
|
|
1209
|
+
*/
|
|
1210
|
+
const POD_SECURITY_PATCH_KEYS: Set<string> = new Set<string>(
|
|
1211
|
+
[
|
|
1212
|
+
"serviceAccountName",
|
|
1213
|
+
"serviceAccount",
|
|
1214
|
+
"automountServiceAccountToken",
|
|
1215
|
+
"securityContext",
|
|
1216
|
+
"privileged",
|
|
1217
|
+
"capabilities",
|
|
1218
|
+
"hostPath",
|
|
1219
|
+
"hostNetwork",
|
|
1220
|
+
"hostPID",
|
|
1221
|
+
"hostIPC",
|
|
1222
|
+
"hostPort",
|
|
1223
|
+
"hostUsers",
|
|
1224
|
+
"runtimeClassName",
|
|
1225
|
+
"shareProcessNamespace",
|
|
1226
|
+
"volumes",
|
|
1227
|
+
"volumeMounts",
|
|
1228
|
+
"initContainers",
|
|
1229
|
+
"ephemeralContainers",
|
|
1230
|
+
"command",
|
|
1231
|
+
"args",
|
|
1232
|
+
"envFrom",
|
|
1233
|
+
"secretKeyRef",
|
|
1234
|
+
"secretRef",
|
|
1235
|
+
"secretName",
|
|
1236
|
+
].map((key: string) => {
|
|
1237
|
+
return key.toLowerCase();
|
|
1238
|
+
}),
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
/*
|
|
1242
|
+
* kubectl create's subcommands and their aliases, exactly as cobra matches
|
|
1243
|
+
* them (case-sensitively). Anything else is not a create subcommand:
|
|
1244
|
+
* kubectl would look for a `kubectl-create-<name>` plugin on the Runner's
|
|
1245
|
+
* PATH, so it is Denied rather than tiered.
|
|
1246
|
+
*/
|
|
1247
|
+
const CREATE_SUBCOMMANDS: Record<string, string> = {
|
|
1248
|
+
namespace: "namespace",
|
|
1249
|
+
ns: "namespace",
|
|
1250
|
+
quota: "resourcequota",
|
|
1251
|
+
resourcequota: "resourcequota",
|
|
1252
|
+
secret: "secret",
|
|
1253
|
+
configmap: "configmap",
|
|
1254
|
+
cm: "configmap",
|
|
1255
|
+
serviceaccount: "serviceaccount",
|
|
1256
|
+
sa: "serviceaccount",
|
|
1257
|
+
service: "service",
|
|
1258
|
+
svc: "service",
|
|
1259
|
+
deployment: "deployment",
|
|
1260
|
+
deploy: "deployment",
|
|
1261
|
+
clusterrole: "clusterrole",
|
|
1262
|
+
clusterrolebinding: "clusterrolebinding",
|
|
1263
|
+
role: "role",
|
|
1264
|
+
rolebinding: "rolebinding",
|
|
1265
|
+
poddisruptionbudget: "poddisruptionbudget",
|
|
1266
|
+
pdb: "poddisruptionbudget",
|
|
1267
|
+
priorityclass: "priorityclass",
|
|
1268
|
+
pc: "priorityclass",
|
|
1269
|
+
job: "job",
|
|
1270
|
+
cronjob: "cronjob",
|
|
1271
|
+
cj: "cronjob",
|
|
1272
|
+
ingress: "ingress",
|
|
1273
|
+
ing: "ingress",
|
|
1274
|
+
token: "token",
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
/*
|
|
1278
|
+
* What `kubectl create <subcommand>` makes that OneUptime AI never makes:
|
|
1279
|
+
* RBAC objects and ServiceAccounts decide who may do what, and a new
|
|
1280
|
+
* ServiceAccount is an identity for someone to bind.
|
|
1281
|
+
*/
|
|
1282
|
+
const DENIED_CREATE_KINDS: Set<string> = new Set<string>([
|
|
1283
|
+
"clusterrole",
|
|
1284
|
+
"clusterrolebinding",
|
|
1285
|
+
"role",
|
|
1286
|
+
"rolebinding",
|
|
1287
|
+
"serviceaccount",
|
|
1288
|
+
]);
|
|
1289
|
+
|
|
1290
|
+
/*
|
|
1291
|
+
* `kubectl create` subcommands that start a NEW workload running the image
|
|
1292
|
+
* named in the command: kubectl requires --image for both (it refuses them
|
|
1293
|
+
* without one), so every such command runs an arbitrary image as a new
|
|
1294
|
+
* program in the cluster — Denied, like `create job --image`.
|
|
1295
|
+
*/
|
|
1296
|
+
const IMAGE_WORKLOAD_CREATE_KINDS: Set<string> = new Set<string>([
|
|
1297
|
+
"deployment",
|
|
1298
|
+
"cronjob",
|
|
1299
|
+
]);
|
|
1300
|
+
|
|
1301
|
+
// Cluster-scoped objects `kubectl create` can make (for the namespace rule).
|
|
1302
|
+
const CLUSTER_SCOPED_CREATE_KINDS: Set<string> = new Set<string>([
|
|
1303
|
+
"namespace",
|
|
1304
|
+
"priorityclass",
|
|
1305
|
+
]);
|
|
1306
|
+
|
|
1307
|
+
/*
|
|
1308
|
+
* kubectl set's subcommands OneUptime AI may use (set serviceaccount and set
|
|
1309
|
+
* subject are Denied by name; anything else is not a set subcommand).
|
|
1310
|
+
*/
|
|
1311
|
+
const SET_SUBCOMMANDS: Set<string> = new Set<string>([
|
|
1312
|
+
"env",
|
|
1313
|
+
"image",
|
|
1314
|
+
"resources",
|
|
1315
|
+
"selector",
|
|
1316
|
+
]);
|
|
1317
|
+
|
|
1318
|
+
const KIND_ALIASES: Record<string, string> = {
|
|
1319
|
+
po: "pod",
|
|
1320
|
+
pods: "pod",
|
|
1321
|
+
pod: "pod",
|
|
1322
|
+
jobs: "job",
|
|
1323
|
+
job: "job",
|
|
1324
|
+
cj: "cronjob",
|
|
1325
|
+
cronjobs: "cronjob",
|
|
1326
|
+
cronjob: "cronjob",
|
|
1327
|
+
deploy: "deployment",
|
|
1328
|
+
deployments: "deployment",
|
|
1329
|
+
deployment: "deployment",
|
|
1330
|
+
sts: "statefulset",
|
|
1331
|
+
statefulsets: "statefulset",
|
|
1332
|
+
statefulset: "statefulset",
|
|
1333
|
+
ds: "daemonset",
|
|
1334
|
+
daemonsets: "daemonset",
|
|
1335
|
+
daemonset: "daemonset",
|
|
1336
|
+
rs: "replicaset",
|
|
1337
|
+
replicasets: "replicaset",
|
|
1338
|
+
replicaset: "replicaset",
|
|
1339
|
+
rc: "replicationcontroller",
|
|
1340
|
+
replicationcontrollers: "replicationcontroller",
|
|
1341
|
+
replicationcontroller: "replicationcontroller",
|
|
1342
|
+
hpa: "horizontalpodautoscaler",
|
|
1343
|
+
horizontalpodautoscalers: "horizontalpodautoscaler",
|
|
1344
|
+
horizontalpodautoscaler: "horizontalpodautoscaler",
|
|
1345
|
+
svc: "service",
|
|
1346
|
+
services: "service",
|
|
1347
|
+
service: "service",
|
|
1348
|
+
ep: "endpoints",
|
|
1349
|
+
endpoints: "endpoints",
|
|
1350
|
+
cm: "configmap",
|
|
1351
|
+
configmaps: "configmap",
|
|
1352
|
+
configmap: "configmap",
|
|
1353
|
+
sa: "serviceaccount",
|
|
1354
|
+
serviceaccounts: "serviceaccount",
|
|
1355
|
+
serviceaccount: "serviceaccount",
|
|
1356
|
+
ing: "ingress",
|
|
1357
|
+
ingresses: "ingress",
|
|
1358
|
+
ingress: "ingress",
|
|
1359
|
+
ingressclasses: "ingressclass",
|
|
1360
|
+
ingressclass: "ingressclass",
|
|
1361
|
+
netpol: "networkpolicy",
|
|
1362
|
+
networkpolicies: "networkpolicy",
|
|
1363
|
+
networkpolicy: "networkpolicy",
|
|
1364
|
+
pdb: "poddisruptionbudget",
|
|
1365
|
+
poddisruptionbudgets: "poddisruptionbudget",
|
|
1366
|
+
poddisruptionbudget: "poddisruptionbudget",
|
|
1367
|
+
quota: "resourcequota",
|
|
1368
|
+
resourcequotas: "resourcequota",
|
|
1369
|
+
resourcequota: "resourcequota",
|
|
1370
|
+
limits: "limitrange",
|
|
1371
|
+
limitranges: "limitrange",
|
|
1372
|
+
limitrange: "limitrange",
|
|
1373
|
+
ns: "namespace",
|
|
1374
|
+
namespaces: "namespace",
|
|
1375
|
+
namespace: "namespace",
|
|
1376
|
+
no: "node",
|
|
1377
|
+
nodes: "node",
|
|
1378
|
+
node: "node",
|
|
1379
|
+
pv: "persistentvolume",
|
|
1380
|
+
persistentvolumes: "persistentvolume",
|
|
1381
|
+
persistentvolume: "persistentvolume",
|
|
1382
|
+
pvc: "persistentvolumeclaim",
|
|
1383
|
+
persistentvolumeclaims: "persistentvolumeclaim",
|
|
1384
|
+
persistentvolumeclaim: "persistentvolumeclaim",
|
|
1385
|
+
crd: "customresourcedefinition",
|
|
1386
|
+
crds: "customresourcedefinition",
|
|
1387
|
+
customresourcedefinitions: "customresourcedefinition",
|
|
1388
|
+
customresourcedefinition: "customresourcedefinition",
|
|
1389
|
+
roles: "role",
|
|
1390
|
+
role: "role",
|
|
1391
|
+
rolebindings: "rolebinding",
|
|
1392
|
+
rolebinding: "rolebinding",
|
|
1393
|
+
clusterroles: "clusterrole",
|
|
1394
|
+
clusterrole: "clusterrole",
|
|
1395
|
+
clusterrolebindings: "clusterrolebinding",
|
|
1396
|
+
clusterrolebinding: "clusterrolebinding",
|
|
1397
|
+
sc: "storageclass",
|
|
1398
|
+
storageclasses: "storageclass",
|
|
1399
|
+
storageclass: "storageclass",
|
|
1400
|
+
secrets: "secret",
|
|
1401
|
+
secret: "secret",
|
|
1402
|
+
pc: "priorityclass",
|
|
1403
|
+
priorityclasses: "priorityclass",
|
|
1404
|
+
priorityclass: "priorityclass",
|
|
1405
|
+
apiservices: "apiservice",
|
|
1406
|
+
apiservice: "apiservice",
|
|
1407
|
+
mutatingwebhookconfigurations: "mutatingwebhookconfiguration",
|
|
1408
|
+
mutatingwebhookconfiguration: "mutatingwebhookconfiguration",
|
|
1409
|
+
validatingwebhookconfigurations: "validatingwebhookconfiguration",
|
|
1410
|
+
validatingwebhookconfiguration: "validatingwebhookconfiguration",
|
|
1411
|
+
validatingadmissionpolicies: "validatingadmissionpolicy",
|
|
1412
|
+
validatingadmissionpolicy: "validatingadmissionpolicy",
|
|
1413
|
+
validatingadmissionpolicybindings: "validatingadmissionpolicybinding",
|
|
1414
|
+
validatingadmissionpolicybinding: "validatingadmissionpolicybinding",
|
|
1415
|
+
mutatingadmissionpolicies: "mutatingadmissionpolicy",
|
|
1416
|
+
mutatingadmissionpolicy: "mutatingadmissionpolicy",
|
|
1417
|
+
mutatingadmissionpolicybindings: "mutatingadmissionpolicybinding",
|
|
1418
|
+
mutatingadmissionpolicybinding: "mutatingadmissionpolicybinding",
|
|
1419
|
+
csr: "certificatesigningrequest",
|
|
1420
|
+
certificatesigningrequests: "certificatesigningrequest",
|
|
1421
|
+
certificatesigningrequest: "certificatesigningrequest",
|
|
1422
|
+
runtimeclasses: "runtimeclass",
|
|
1423
|
+
runtimeclass: "runtimeclass",
|
|
1424
|
+
csidrivers: "csidriver",
|
|
1425
|
+
csidriver: "csidriver",
|
|
1426
|
+
csinodes: "csinode",
|
|
1427
|
+
csinode: "csinode",
|
|
1428
|
+
volumeattachments: "volumeattachment",
|
|
1429
|
+
volumeattachment: "volumeattachment",
|
|
1430
|
+
flowschemas: "flowschema",
|
|
1431
|
+
flowschema: "flowschema",
|
|
1432
|
+
prioritylevelconfigurations: "prioritylevelconfiguration",
|
|
1433
|
+
prioritylevelconfiguration: "prioritylevelconfiguration",
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
interface ParsedArgs {
|
|
1437
|
+
positionals: Array<string>;
|
|
1438
|
+
flags: Map<string, Array<string>>; // name -> values (empty string for bare)
|
|
1439
|
+
// The first flag problem seen: a denied flag, an unknown flag or bad syntax.
|
|
1440
|
+
violation?: string | undefined;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
interface FlagRule {
|
|
1444
|
+
arity: FlagArity;
|
|
1445
|
+
/*
|
|
1446
|
+
* Set when the flag may not be used: the tail of the reason, after "the
|
|
1447
|
+
* --x flag ". Parsing still uses the arity so the verb can be found and
|
|
1448
|
+
* named in the message.
|
|
1449
|
+
*/
|
|
1450
|
+
problem?: string | undefined;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
interface NamedKinds {
|
|
1454
|
+
// Normalized kinds (normalizeKind), for the deny lists.
|
|
1455
|
+
kinds: Set<string>;
|
|
1456
|
+
// Each kind as written ("deployments.apps", "po"), for builtinKind().
|
|
1457
|
+
rawKinds: Array<string>;
|
|
1458
|
+
// How many objects are named (as opposed to a bare kind meaning "all").
|
|
1459
|
+
namedCount: number;
|
|
1460
|
+
// The names, in order ("web" from both "pod web" and "pod/web").
|
|
1461
|
+
names: Array<string>;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/*
|
|
1465
|
+
* The kind a token names, for the DENY lists: lowercased, the head of a
|
|
1466
|
+
* group-qualified form ("pods.v1." / "deployments.apps" / "secrets.foo.io"
|
|
1467
|
+
* all keep the leading segment) mapped through KIND_ALIASES. Folding every
|
|
1468
|
+
* group into the built-in kind is the safe side for a deny list — a custom
|
|
1469
|
+
* `secrets.example.io` is refused like a Secret — and for a rule that can
|
|
1470
|
+
* only ADD a human (a Namespace object named kube-system). It must never be
|
|
1471
|
+
* used to decide that something is SAFE, or that a write is cluster-scoped
|
|
1472
|
+
* and so outside the namespace -n names (see builtinKind()).
|
|
1473
|
+
*/
|
|
1474
|
+
function normalizeKind(token: string): string {
|
|
1475
|
+
const lower: string = token.toLowerCase();
|
|
1476
|
+
const head: string = lower.split(".")[0] || lower;
|
|
1477
|
+
return Object.prototype.hasOwnProperty.call(KIND_ALIASES, head)
|
|
1478
|
+
? KIND_ALIASES[head]!
|
|
1479
|
+
: head;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/*
|
|
1483
|
+
* The built-in kind a token names, for the SAFE sets and the cluster-scoped
|
|
1484
|
+
* rule — or undefined when it is not one of BUILTIN_KIND_GROUPS in that
|
|
1485
|
+
* kind's own API group. kubectl (schema.ParseResourceArg) reads a token with
|
|
1486
|
+
* two dots or more first as "RESOURCE.VERSION.GROUP" and then as
|
|
1487
|
+
* "RESOURCE.GROUP", and one with a single dot only as "RESOURCE.GROUP"
|
|
1488
|
+
* ("pods." and "pods.v1." name the core group). So `deployments.apps`,
|
|
1489
|
+
* `deployments.v1.apps`, `pods.v1.`, `storageclasses.storage.k8s.io` and
|
|
1490
|
+
* `clusterroles.v1.rbac.authorization.k8s.io` are the built-in kinds, and
|
|
1491
|
+
* `pods.example.com`, `nodes.v1.example.com` or `jobs.batch.volcano.sh` are
|
|
1492
|
+
* custom resources.
|
|
1493
|
+
*/
|
|
1494
|
+
function builtinKind(token: string): string | undefined {
|
|
1495
|
+
const lower: string = token.toLowerCase();
|
|
1496
|
+
const parts: Array<string> = lower.split(".");
|
|
1497
|
+
const head: string = parts[0] || "";
|
|
1498
|
+
|
|
1499
|
+
if (!Object.prototype.hasOwnProperty.call(KIND_ALIASES, head)) {
|
|
1500
|
+
return undefined;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const kind: string = KIND_ALIASES[head]!;
|
|
1504
|
+
|
|
1505
|
+
if (!Object.prototype.hasOwnProperty.call(BUILTIN_KIND_GROUPS, kind)) {
|
|
1506
|
+
return undefined;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (parts.length === 1) {
|
|
1510
|
+
return kind;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const group: string = BUILTIN_KIND_GROUPS[kind]!;
|
|
1514
|
+
|
|
1515
|
+
// RESOURCE.GROUP
|
|
1516
|
+
if (parts.slice(1).join(".") === group) {
|
|
1517
|
+
return kind;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// RESOURCE.VERSION.GROUP
|
|
1521
|
+
if (parts.length >= 3 && parts.slice(2).join(".") === group) {
|
|
1522
|
+
return kind;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return undefined;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/*
|
|
1529
|
+
* True when every object a node verb names is a node: a bare NAME, or a
|
|
1530
|
+
* TYPE/NAME whose type is the built-in Node (kubectl's cordon, uncordon and
|
|
1531
|
+
* drain read "nodes/NAME" and also accept any other TYPE/NAME, such as a
|
|
1532
|
+
* custom `nodes.example.com/x`, which then is not a node at all).
|
|
1533
|
+
*/
|
|
1534
|
+
function namesOnlyNodes(nodeTokens: Array<string>): boolean {
|
|
1535
|
+
return nodeTokens.every((token: string) => {
|
|
1536
|
+
const slash: number = token.indexOf("/");
|
|
1537
|
+
return slash < 0 || builtinKind(token.slice(0, slash)) === "node";
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/*
|
|
1542
|
+
* The kinds named by the positionals after a verb, read the way kubectl's
|
|
1543
|
+
* resource builder reads them: either "kind name1 name2" (the first token
|
|
1544
|
+
* is the kind, possibly "pod,job"; the rest are names) or "kind/name
|
|
1545
|
+
* kind2/name2" (every token carries its own kind). Mixing the two is an
|
|
1546
|
+
* error in kubectl; here it just names both kinds, which is the safe side.
|
|
1547
|
+
*/
|
|
1548
|
+
function namedKinds(resourceTokens: Array<string>): NamedKinds {
|
|
1549
|
+
const kinds: Set<string> = new Set<string>();
|
|
1550
|
+
const rawKinds: Array<string> = [];
|
|
1551
|
+
const names: Array<string> = [];
|
|
1552
|
+
let namedCount: number = 0;
|
|
1553
|
+
|
|
1554
|
+
for (let i: number = 0; i < resourceTokens.length; i++) {
|
|
1555
|
+
const token: string = resourceTokens[i]!;
|
|
1556
|
+
if (token.includes("/")) {
|
|
1557
|
+
const slash: number = token.indexOf("/");
|
|
1558
|
+
const rawKind: string = token.slice(0, slash);
|
|
1559
|
+
rawKinds.push(rawKind);
|
|
1560
|
+
kinds.add(normalizeKind(rawKind));
|
|
1561
|
+
names.push(token.slice(slash + 1));
|
|
1562
|
+
namedCount++;
|
|
1563
|
+
} else if (i === 0) {
|
|
1564
|
+
for (const part of token.split(",")) {
|
|
1565
|
+
rawKinds.push(part);
|
|
1566
|
+
kinds.add(normalizeKind(part));
|
|
1567
|
+
}
|
|
1568
|
+
} else {
|
|
1569
|
+
names.push(token);
|
|
1570
|
+
namedCount++;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
return { kinds, rawKinds, namedCount, names };
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/*
|
|
1578
|
+
* True when the objects are exactly ONE named object of ONE kind — the
|
|
1579
|
+
* shape every SafeWrite needs. A bare kind is every object of that kind to
|
|
1580
|
+
* kubectl's rollout verbs, and a comma list of kinds or several names is
|
|
1581
|
+
* several objects.
|
|
1582
|
+
*/
|
|
1583
|
+
function isOneNamedObject(targets: NamedKinds): boolean {
|
|
1584
|
+
return targets.namedCount === 1 && targets.rawKinds.length === 1;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// True when every kind is built in and in `allowed` (see builtinKind()).
|
|
1588
|
+
function everyBuiltinKindIn(
|
|
1589
|
+
targets: NamedKinds,
|
|
1590
|
+
allowed: Set<string>,
|
|
1591
|
+
): boolean {
|
|
1592
|
+
return (
|
|
1593
|
+
targets.rawKinds.length > 0 &&
|
|
1594
|
+
targets.rawKinds.every((rawKind: string) => {
|
|
1595
|
+
const kind: string | undefined = builtinKind(rawKind);
|
|
1596
|
+
return kind !== undefined && allowed.has(kind);
|
|
1597
|
+
})
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/*
|
|
1602
|
+
* The commands that read the words after their objects as updates, KEY=VALUE
|
|
1603
|
+
* or KEY- (kubectl's GetResourcesAndPairs for label, annotate and set image,
|
|
1604
|
+
* SplitEnvironmentFromResources for set env, and taint's own split): for
|
|
1605
|
+
* these, and only these, the objects end at the first such word. (taint also
|
|
1606
|
+
* ends them at a KEY:EFFECT word; reading that as one more object only adds
|
|
1607
|
+
* an object.) Every other verb hands kubectl's resource builder ALL of its
|
|
1608
|
+
* words, `=` or not, and kubectl goes on to the next object when one fails:
|
|
1609
|
+
* checked with the pinned kubectl v1.36.4 against a fake API server, `get
|
|
1610
|
+
* pod/a=b secret/s` prints the Secret and `patch pod/a=b node/n1` patches
|
|
1611
|
+
* the Node, each after the missing pod's error.
|
|
1612
|
+
*/
|
|
1613
|
+
function readsObjectsThenPairs(verb: string, subcommand: string): boolean {
|
|
1614
|
+
if (verb === "label" || verb === "annotate" || verb === "taint") {
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
return verb === "set" && (subcommand === "image" || subcommand === "env");
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/*
|
|
1622
|
+
* The object tokens of a command, split the way kubectl splits positionals:
|
|
1623
|
+
* the words after the verb (and the subcommand, for rollout and set) — for
|
|
1624
|
+
* the commands that take updates (readsObjectsThenPairs), only the leading
|
|
1625
|
+
* words that are not KEY=VALUE or KEY- pairs. So `label pod web-1 app=web`
|
|
1626
|
+
* names only the pod, and `label pod web-1 secret/rotated=true` names no
|
|
1627
|
+
* Secret, while `get pod/a=b secret/s` names the Secret. For set selector
|
|
1628
|
+
* the last word is the selector expression, not an object; it is kept here,
|
|
1629
|
+
* which can only add an object to what the deny lists read.
|
|
1630
|
+
*/
|
|
1631
|
+
function objectTokens(verb: string, positionals: Array<string>): Array<string> {
|
|
1632
|
+
const start: number = verb === "rollout" || verb === "set" ? 2 : 1;
|
|
1633
|
+
const words: Array<string> = positionals.slice(start);
|
|
1634
|
+
|
|
1635
|
+
if (!readsObjectsThenPairs(verb, (positionals[1] || "").toLowerCase())) {
|
|
1636
|
+
return words;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const tokens: Array<string> = [];
|
|
1640
|
+
|
|
1641
|
+
for (const token of words) {
|
|
1642
|
+
if (token.includes("=") || token.endsWith("-")) {
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1645
|
+
tokens.push(token);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
return tokens;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/*
|
|
1652
|
+
* The objects a write names: after the verb for delete, after the verb's
|
|
1653
|
+
* object tokens for the other object verbs (after the subcommand for
|
|
1654
|
+
* rollout and set), and none for node verbs and create. evaluateArgs and
|
|
1655
|
+
* the allowlist's reading of an entry (allowlistMayPromote) both use this,
|
|
1656
|
+
* so the two can never disagree about what a command acts on.
|
|
1657
|
+
*/
|
|
1658
|
+
function commandTargets(verb: string, positionals: Array<string>): NamedKinds {
|
|
1659
|
+
if (verb === "delete") {
|
|
1660
|
+
return namedKinds(positionals.slice(1));
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
return OBJECT_VERBS.has(verb)
|
|
1664
|
+
? namedKinds(objectTokens(verb, positionals))
|
|
1665
|
+
: namedKinds([]);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/*
|
|
1669
|
+
* True when one of the objects is the built-in Node, in any spelling kubectl
|
|
1670
|
+
* resolves to it (builtinKind(): node, nodes, no, Node, nodes.v1., no/NAME).
|
|
1671
|
+
* A custom resource that borrows the name (`nodes.example.com`) is not.
|
|
1672
|
+
*/
|
|
1673
|
+
function namesBuiltinNode(targets: NamedKinds): boolean {
|
|
1674
|
+
return targets.rawKinds.some((rawKind: string) => {
|
|
1675
|
+
return builtinKind(rawKind) === "node";
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/*
|
|
1680
|
+
* Why nothing an operator sets on the cluster runs this write unattended —
|
|
1681
|
+
* a drain, a taint, or a patch of a Node (see the header) — or undefined.
|
|
1682
|
+
* A write that gets a reason here carries `requiresHuman`, and an allowlist
|
|
1683
|
+
* entry that names one pre-approves nothing.
|
|
1684
|
+
*/
|
|
1685
|
+
function describeAlwaysHumanWrite(
|
|
1686
|
+
verb: string,
|
|
1687
|
+
targets: NamedKinds,
|
|
1688
|
+
): string | undefined {
|
|
1689
|
+
if (Object.prototype.hasOwnProperty.call(ALWAYS_HUMAN_VERB_REASONS, verb)) {
|
|
1690
|
+
return ALWAYS_HUMAN_VERB_REASONS[verb];
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (verb === "patch" && namesBuiltinNode(targets)) {
|
|
1694
|
+
return NODE_PATCH_ALWAYS_HUMAN_REASON;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
return undefined;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/*
|
|
1701
|
+
* The label / annotation KEYS a label or annotate command writes or removes:
|
|
1702
|
+
* the pair tokens after the objects, "KEY=VALUE" (the key is before the
|
|
1703
|
+
* first "=") or "KEY-" (removal). Values are never inspected, so a reserved
|
|
1704
|
+
* string in a value is just text.
|
|
1705
|
+
*/
|
|
1706
|
+
function labelKeys(verb: string, positionals: Array<string>): Array<string> {
|
|
1707
|
+
const start: number = 1 + objectTokens(verb, positionals).length;
|
|
1708
|
+
const keys: Array<string> = [];
|
|
1709
|
+
|
|
1710
|
+
for (const token of positionals.slice(start)) {
|
|
1711
|
+
const eq: number = token.indexOf("=");
|
|
1712
|
+
if (eq >= 0) {
|
|
1713
|
+
keys.push(token.slice(0, eq));
|
|
1714
|
+
} else if (token.endsWith("-")) {
|
|
1715
|
+
keys.push(token.slice(0, -1));
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
return keys;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// The DNS prefix of a label key ("pod-security.kubernetes.io/enforce"), lowercased.
|
|
1723
|
+
function keyDomain(key: string): string {
|
|
1724
|
+
const slash: number = key.indexOf("/");
|
|
1725
|
+
return slash >= 0 ? key.slice(0, slash).toLowerCase() : "";
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function isDomainOrSubdomain(domain: string, of: string): boolean {
|
|
1729
|
+
return domain === of || domain.endsWith(`.${of}`);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function isReservedKey(key: string): boolean {
|
|
1733
|
+
const domain: string = keyDomain(key);
|
|
1734
|
+
return (
|
|
1735
|
+
domain.length > 0 &&
|
|
1736
|
+
RESERVED_KEY_DOMAINS.some((reserved: string) => {
|
|
1737
|
+
return isDomainOrSubdomain(domain, reserved);
|
|
1738
|
+
})
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function isPodSecurityKey(key: string): boolean {
|
|
1743
|
+
return isDomainOrSubdomain(keyDomain(key), POD_SECURITY_KEY_DOMAIN);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function isAppArmorAnnotationKey(key: string): boolean {
|
|
1747
|
+
return isDomainOrSubdomain(keyDomain(key), APPARMOR_ANNOTATION_KEY_DOMAIN);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function isRbacAggregationKey(key: string): boolean {
|
|
1751
|
+
return key.toLowerCase().startsWith(RBAC_AGGREGATION_KEY_PREFIX);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
const MAX_PATCH_BODY_DEPTH: number = 64;
|
|
1755
|
+
|
|
1756
|
+
/*
|
|
1757
|
+
* findForbiddenJsonField's answer for a body nested past
|
|
1758
|
+
* MAX_PATCH_BODY_DEPTH. It can never equal a field it reports, which are
|
|
1759
|
+
* POD_SECURITY_PATCH_KEYS, Pod Security Admission label keys, AppArmor
|
|
1760
|
+
* annotation keys and a probe's or hook's host (httpGet.host).
|
|
1761
|
+
*/
|
|
1762
|
+
const PATCH_BODY_TOO_DEEP: string = "\0too-deep";
|
|
1763
|
+
|
|
1764
|
+
/*
|
|
1765
|
+
* Why a `kubectl patch` body may not run, or null. The body must be JSON,
|
|
1766
|
+
* and the JSON is walked for POD_SECURITY_PATCH_KEYS, Pod Security
|
|
1767
|
+
* Admission labels, AppArmor annotations and the host of a probe or
|
|
1768
|
+
* lifecycle hook (keys, and the path/from of JSON-patch operations,
|
|
1769
|
+
* decoded, so a \u escape or a ~1 cannot hide a name).
|
|
1770
|
+
*
|
|
1771
|
+
* Why JSON only: kubectl sends a body whose first non-space byte is "{" to
|
|
1772
|
+
* the API server byte for byte, and decodes every other body as YAML 1.1
|
|
1773
|
+
* (apimachinery's yaml.ToJSON) — including a "[" body such as a JSON patch.
|
|
1774
|
+
* YAML honours type tags, anchors, aliases and merge keys, so `!!binary
|
|
1775
|
+
* c2VydmljZUFjY291bnROYW1l` is the key serviceAccountName although that
|
|
1776
|
+
* word appears nowhere in the text; no scan of the text can see what
|
|
1777
|
+
* kubectl will apply. A body that IS valid JSON decodes to the same keys
|
|
1778
|
+
* and paths either way: in JSON, tag, anchor and merge-key syntax can only
|
|
1779
|
+
* sit inside quoted strings, where YAML reads it as plain text, and a
|
|
1780
|
+
* repeated key keeps its last value in JSON.parse and in kubectl alike.
|
|
1781
|
+
* Every patch a fix needs can be written as JSON.
|
|
1782
|
+
*
|
|
1783
|
+
* A body that names no forbidden field can still drop them by replacing or
|
|
1784
|
+
* removing what holds them; findPodSpecReplacement refuses that, for the
|
|
1785
|
+
* patch types kubectl could apply the body as (`patchTypes`, see
|
|
1786
|
+
* readPatchTypes).
|
|
1787
|
+
*/
|
|
1788
|
+
function findPatchBodyProblem(
|
|
1789
|
+
body: string,
|
|
1790
|
+
patchTypes: Set<PatchType>,
|
|
1791
|
+
): string | null {
|
|
1792
|
+
let parsed: unknown = undefined;
|
|
1793
|
+
|
|
1794
|
+
try {
|
|
1795
|
+
parsed = JSON.parse(body);
|
|
1796
|
+
} catch {
|
|
1797
|
+
return `kubectl patch with a body that is not JSON is never allowed for OneUptime AI: kubectl reads any other body as YAML, whose tags, anchors and merge keys can spell a field name that appears nowhere in the text (write the patch body as JSON, e.g. -p '{"spec":{"replicas":3}}')`;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const field: string | null = findForbiddenJsonField(parsed, 0, "");
|
|
1801
|
+
|
|
1802
|
+
if (field === PATCH_BODY_TOO_DEEP) {
|
|
1803
|
+
return `kubectl patch with a body nested more than ${MAX_PATCH_BODY_DEPTH} levels deep is never allowed for OneUptime AI (no fix needs one)`;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
if (field !== null) {
|
|
1807
|
+
return isPodSecurityKey(field)
|
|
1808
|
+
? `kubectl patch of ${field} is never allowed for OneUptime AI: Pod Security Admission reads these labels to decide whether privileged pods may run in a namespace`
|
|
1809
|
+
: `kubectl patch that touches ${field} is never allowed for OneUptime AI: it changes which identity, privileges, host access, Secrets or program a pod runs with (patch other fields, or use set image / set env / set resources)`;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
return findPodSpecReplacement(parsed, patchTypes);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/*
|
|
1816
|
+
* ---- Patches that replace what holds the pod-security fields ---------------
|
|
1817
|
+
*
|
|
1818
|
+
* POD_SECURITY_PATCH_KEYS are matched by NAME, which only sees a patch that
|
|
1819
|
+
* spells one. A patch that replaces or removes an object ABOVE them drops
|
|
1820
|
+
* them without naming any. Checked against the pinned kubectl v1.36.4 with
|
|
1821
|
+
* `patch --local` on a Deployment whose pod runs as a restricted
|
|
1822
|
+
* ServiceAccount with a securityContext, a command and a volume:
|
|
1823
|
+
* - strategic merge (the default type): `"$patch": "replace"` in the pod
|
|
1824
|
+
* spec (or in template, or spec) leaves only what the patch lists —
|
|
1825
|
+
* serviceAccountName back to default, securityContext and volumes
|
|
1826
|
+
* gone; `$retainKeys` in the pod spec clears every field it does not
|
|
1827
|
+
* list; a `{"$patch": "replace"}` element in the containers list drops
|
|
1828
|
+
* every container's command, securityContext, env and volumeMounts; a
|
|
1829
|
+
* `{"name": X, "$patch": "delete"}` element removes container X; null
|
|
1830
|
+
* for the pod spec or the containers list removes it;
|
|
1831
|
+
* - merge (--type=merge, RFC 7386): a list is replaced wholesale, so
|
|
1832
|
+
* `{"containers": [{"name": "web", "image": "x"}]}` leaves one container
|
|
1833
|
+
* with nothing but a name and an image; null removes, and a scalar
|
|
1834
|
+
* replaces;
|
|
1835
|
+
* - JSON patch: replace or remove of /spec/template/spec,
|
|
1836
|
+
* /spec/template/spec/containers or /spec/template/spec/containers/0
|
|
1837
|
+
* (/spec/containers/0 on a Pod), and an add whose path is an existing
|
|
1838
|
+
* object member — add replaces it — do the same.
|
|
1839
|
+
*
|
|
1840
|
+
* So nothing on the path from the object down to a pod-security field may be
|
|
1841
|
+
* replaced, removed, reordered or filtered. That path is POD_SPEC_PATH_STEPS
|
|
1842
|
+
* from the top of the body: through spec, template and jobTemplate (the pod
|
|
1843
|
+
* template of a Pod, a workload, a Job, a CronJob, a PodTemplate, and the
|
|
1844
|
+
* custom workloads that copy that layout) to the pod spec, then its
|
|
1845
|
+
* containers list, one container, its env list, one env entry and its
|
|
1846
|
+
* valueFrom. Refused along that path:
|
|
1847
|
+
* - any strategic-merge directive — a key starting with "$" ($patch,
|
|
1848
|
+
* $retainKeys, $setElementOrder/..., $deleteFromPrimitiveList/...) — in
|
|
1849
|
+
* a map on it, a `{"$patch": ...}` element of a list on it included;
|
|
1850
|
+
* - null for a key on it, or a value of the wrong shape (a scalar or a
|
|
1851
|
+
* list where the object goes, anything but a list where a list goes);
|
|
1852
|
+
* - under a merge patch, any value for the containers or env list;
|
|
1853
|
+
* - a JSON-patch remove, replace, move or copy whose path or from is on it
|
|
1854
|
+
* (the whole object, "", included), and an add whose path is on it and
|
|
1855
|
+
* names an object member rather than a list position.
|
|
1856
|
+
* Anything off that path — metadata and the pod template's labels and
|
|
1857
|
+
* annotations, replicas, the update strategy, a container's image or
|
|
1858
|
+
* resources, an env entry's value — stays patchable: a strategic-merge patch
|
|
1859
|
+
* that names one container and sets its image or resources, a JSON-patch
|
|
1860
|
+
* replace of /spec/replicas or /spec/template/spec/containers/0/image, a
|
|
1861
|
+
* merge patch of metadata/annotations. Adding a list element (a new env
|
|
1862
|
+
* entry, or a new container, whose fields the name check reads) is not a
|
|
1863
|
+
* replacement. Keys are matched case-insensitively, like the name check.
|
|
1864
|
+
*/
|
|
1865
|
+
|
|
1866
|
+
// The patch types kubectl patch accepts for --type (it lowercases the value).
|
|
1867
|
+
enum PatchType {
|
|
1868
|
+
Strategic = "strategic",
|
|
1869
|
+
Merge = "merge",
|
|
1870
|
+
Json = "json",
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const PATCH_TYPES: Array<PatchType> = [
|
|
1874
|
+
PatchType.Strategic,
|
|
1875
|
+
PatchType.Merge,
|
|
1876
|
+
PatchType.Json,
|
|
1877
|
+
];
|
|
1878
|
+
|
|
1879
|
+
/*
|
|
1880
|
+
* The types kubectl could apply a body as: strategic merge when --type is
|
|
1881
|
+
* not given, else every --type value (pflag keeps the last one; reading all
|
|
1882
|
+
* of them is the safe side). A value kubectl does not know makes it refuse
|
|
1883
|
+
* the command, so it is read as every type.
|
|
1884
|
+
*/
|
|
1885
|
+
function readPatchTypes(typeValues: Array<string>): Set<PatchType> {
|
|
1886
|
+
const types: Set<PatchType> = new Set<PatchType>();
|
|
1887
|
+
|
|
1888
|
+
if (typeValues.length === 0) {
|
|
1889
|
+
types.add(PatchType.Strategic);
|
|
1890
|
+
return types;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
for (const value of typeValues) {
|
|
1894
|
+
const known: PatchType | undefined = PATCH_TYPES.find((type: PatchType) => {
|
|
1895
|
+
return type === value.trim().toLowerCase();
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
if (known) {
|
|
1899
|
+
types.add(known);
|
|
1900
|
+
} else {
|
|
1901
|
+
for (const type of PATCH_TYPES) {
|
|
1902
|
+
types.add(type);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
return types;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// Where a patch is on the path from the object to the pod-security fields.
|
|
1911
|
+
enum PodSpecPathStep {
|
|
1912
|
+
// The object, spec, template, jobTemplate — and the pod spec itself.
|
|
1913
|
+
PodSpecOrAbove = "PodSpecOrAbove",
|
|
1914
|
+
ContainerList = "ContainerList",
|
|
1915
|
+
Container = "Container",
|
|
1916
|
+
EnvList = "EnvList",
|
|
1917
|
+
EnvEntry = "EnvEntry",
|
|
1918
|
+
ValueFrom = "ValueFrom",
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/*
|
|
1922
|
+
* The steps down the path, by lowercased key. A list step's elements are the
|
|
1923
|
+
* step in LIST_ELEMENT_STEPS. A key not listed for a step leaves the path.
|
|
1924
|
+
*/
|
|
1925
|
+
const POD_SPEC_PATH_STEPS: Record<
|
|
1926
|
+
PodSpecPathStep,
|
|
1927
|
+
Record<string, PodSpecPathStep>
|
|
1928
|
+
> = {
|
|
1929
|
+
[PodSpecPathStep.PodSpecOrAbove]: {
|
|
1930
|
+
spec: PodSpecPathStep.PodSpecOrAbove,
|
|
1931
|
+
template: PodSpecPathStep.PodSpecOrAbove,
|
|
1932
|
+
jobtemplate: PodSpecPathStep.PodSpecOrAbove,
|
|
1933
|
+
containers: PodSpecPathStep.ContainerList,
|
|
1934
|
+
},
|
|
1935
|
+
[PodSpecPathStep.ContainerList]: {},
|
|
1936
|
+
[PodSpecPathStep.Container]: { env: PodSpecPathStep.EnvList },
|
|
1937
|
+
[PodSpecPathStep.EnvList]: {},
|
|
1938
|
+
[PodSpecPathStep.EnvEntry]: { valuefrom: PodSpecPathStep.ValueFrom },
|
|
1939
|
+
[PodSpecPathStep.ValueFrom]: {},
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
const LIST_ELEMENT_STEPS: Map<PodSpecPathStep, PodSpecPathStep> = new Map<
|
|
1943
|
+
PodSpecPathStep,
|
|
1944
|
+
PodSpecPathStep
|
|
1945
|
+
>([
|
|
1946
|
+
[PodSpecPathStep.ContainerList, PodSpecPathStep.Container],
|
|
1947
|
+
[PodSpecPathStep.EnvList, PodSpecPathStep.EnvEntry],
|
|
1948
|
+
]);
|
|
1949
|
+
|
|
1950
|
+
function nextPodSpecPathStep(
|
|
1951
|
+
step: PodSpecPathStep,
|
|
1952
|
+
key: string,
|
|
1953
|
+
): PodSpecPathStep | undefined {
|
|
1954
|
+
const listElement: PodSpecPathStep | undefined = LIST_ELEMENT_STEPS.get(step);
|
|
1955
|
+
|
|
1956
|
+
if (listElement) {
|
|
1957
|
+
return listElement;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const steps: Record<string, PodSpecPathStep> = POD_SPEC_PATH_STEPS[step];
|
|
1961
|
+
const lower: string = key.toLowerCase();
|
|
1962
|
+
|
|
1963
|
+
return Object.prototype.hasOwnProperty.call(steps, lower)
|
|
1964
|
+
? steps[lower]
|
|
1965
|
+
: undefined;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// What the refusals say a replacement drops.
|
|
1969
|
+
const POD_SPEC_REPLACEMENT_DROPS: string =
|
|
1970
|
+
"it replaces or removes a pod spec, a container or what holds them, and so drops the securityContext, serviceAccountName, command, args, volumes, volumeMounts and Secret references under it without naming any of them";
|
|
1971
|
+
|
|
1972
|
+
const POD_SPEC_REPLACEMENT_INSTEAD: string =
|
|
1973
|
+
"patch the field itself (such as one container's image or resources, named in a strategic-merge patch, or /spec/template/spec/containers/0/image in a JSON patch), or use set image / set env / set resources";
|
|
1974
|
+
|
|
1975
|
+
// A JSON-pointer segment: "~1" is "/" and "~0" is "~" (RFC 6901, in that order).
|
|
1976
|
+
const JSON_POINTER_SLASH_ESCAPE: RegExp = /~1/g;
|
|
1977
|
+
const JSON_POINTER_TILDE_ESCAPE: RegExp = /~0/g;
|
|
1978
|
+
|
|
1979
|
+
function decodeJsonPointerSegment(segment: string): string {
|
|
1980
|
+
return segment
|
|
1981
|
+
.replace(JSON_POINTER_SLASH_ESCAPE, "/")
|
|
1982
|
+
.replace(JSON_POINTER_TILDE_ESCAPE, "~");
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// A list position in a JSON-patch path: an index, or "-" (the end).
|
|
1986
|
+
const JSON_PATCH_LIST_POSITION: RegExp = /^(-|[0-9]+)$/;
|
|
1987
|
+
|
|
1988
|
+
function describePodSpecPath(segments: Array<string>): string {
|
|
1989
|
+
return segments.length === 0 ? "the whole object" : `/${segments.join("/")}`;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
1993
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function isListStep(step: PodSpecPathStep): boolean {
|
|
1997
|
+
return LIST_ELEMENT_STEPS.has(step);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/*
|
|
2001
|
+
* Why a parsed patch body replaces or removes something on the pod-spec path
|
|
2002
|
+
* (see the section above), or null.
|
|
2003
|
+
*/
|
|
2004
|
+
function findPodSpecReplacement(
|
|
2005
|
+
parsed: unknown,
|
|
2006
|
+
patchTypes: Set<PatchType>,
|
|
2007
|
+
): string | null {
|
|
2008
|
+
if (Array.isArray(parsed)) {
|
|
2009
|
+
for (const operation of parsed) {
|
|
2010
|
+
if (!isPlainObject(operation)) {
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
const problem: string | null = findJsonPatchReplacement(operation);
|
|
2015
|
+
|
|
2016
|
+
if (problem) {
|
|
2017
|
+
return problem;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
if (!isPlainObject(parsed)) {
|
|
2024
|
+
return null;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
return findObjectPatchReplacement(
|
|
2028
|
+
parsed,
|
|
2029
|
+
PodSpecPathStep.PodSpecOrAbove,
|
|
2030
|
+
[],
|
|
2031
|
+
patchTypes,
|
|
2032
|
+
0,
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// A strategic-merge or merge body, walked down the pod-spec path only.
|
|
2037
|
+
function findObjectPatchReplacement(
|
|
2038
|
+
map: Record<string, unknown>,
|
|
2039
|
+
step: PodSpecPathStep,
|
|
2040
|
+
segments: Array<string>,
|
|
2041
|
+
patchTypes: Set<PatchType>,
|
|
2042
|
+
depth: number,
|
|
2043
|
+
): string | null {
|
|
2044
|
+
// findForbiddenJsonField already refused a body this deep.
|
|
2045
|
+
if (depth > MAX_PATCH_BODY_DEPTH) {
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
for (const [key, value] of Object.entries(map)) {
|
|
2050
|
+
if (key.startsWith("$")) {
|
|
2051
|
+
return `kubectl patch with the strategic-merge directive "${key}" at ${describePodSpecPath(segments)} is never allowed for OneUptime AI: ${POD_SPEC_REPLACEMENT_DROPS} (${POD_SPEC_REPLACEMENT_INSTEAD})`;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
const next: PodSpecPathStep | undefined = nextPodSpecPathStep(step, key);
|
|
2055
|
+
|
|
2056
|
+
if (next === undefined) {
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const where: string = describePodSpecPath([...segments, key]);
|
|
2061
|
+
|
|
2062
|
+
if (value === null) {
|
|
2063
|
+
return `kubectl patch that sets ${where} to null is never allowed for OneUptime AI: ${POD_SPEC_REPLACEMENT_DROPS} (${POD_SPEC_REPLACEMENT_INSTEAD})`;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
if (!isListStep(next)) {
|
|
2067
|
+
if (!isPlainObject(value)) {
|
|
2068
|
+
return `kubectl patch that sets ${where} to something other than an object is never allowed for OneUptime AI: ${POD_SPEC_REPLACEMENT_DROPS} (${POD_SPEC_REPLACEMENT_INSTEAD})`;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const problem: string | null = findObjectPatchReplacement(
|
|
2072
|
+
value,
|
|
2073
|
+
next,
|
|
2074
|
+
[...segments, key],
|
|
2075
|
+
patchTypes,
|
|
2076
|
+
depth + 1,
|
|
2077
|
+
);
|
|
2078
|
+
|
|
2079
|
+
if (problem) {
|
|
2080
|
+
return problem;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
if (patchTypes.has(PatchType.Merge)) {
|
|
2087
|
+
return `kubectl patch --type=merge that sets ${where} is never allowed for OneUptime AI: a merge patch replaces a list wholesale, so ${POD_SPEC_REPLACEMENT_DROPS} (use a strategic-merge patch that names the container, or set image / set env / set resources)`;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
if (!Array.isArray(value)) {
|
|
2091
|
+
return `kubectl patch that sets ${where} to something other than a list is never allowed for OneUptime AI: ${POD_SPEC_REPLACEMENT_DROPS} (${POD_SPEC_REPLACEMENT_INSTEAD})`;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
const element: PodSpecPathStep = LIST_ELEMENT_STEPS.get(next)!;
|
|
2095
|
+
|
|
2096
|
+
for (let index: number = 0; index < value.length; index++) {
|
|
2097
|
+
const item: unknown = value[index];
|
|
2098
|
+
|
|
2099
|
+
if (!isPlainObject(item)) {
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
const problem: string | null = findObjectPatchReplacement(
|
|
2104
|
+
item,
|
|
2105
|
+
element,
|
|
2106
|
+
[...segments, key, String(index)],
|
|
2107
|
+
patchTypes,
|
|
2108
|
+
depth + 2,
|
|
2109
|
+
);
|
|
2110
|
+
|
|
2111
|
+
if (problem) {
|
|
2112
|
+
return problem;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
/*
|
|
2121
|
+
* Where a JSON-patch path lands on the pod-spec path: the step it names and
|
|
2122
|
+
* whether its last segment is a list position — or undefined when it leaves
|
|
2123
|
+
* the path.
|
|
2124
|
+
*/
|
|
2125
|
+
interface JsonPatchTarget {
|
|
2126
|
+
step: PodSpecPathStep;
|
|
2127
|
+
isListPosition: boolean;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function readJsonPatchTarget(pointer: string): JsonPatchTarget | undefined {
|
|
2131
|
+
// "" is the whole object; every other pointer starts with "/".
|
|
2132
|
+
const segments: Array<string> =
|
|
2133
|
+
pointer === ""
|
|
2134
|
+
? []
|
|
2135
|
+
: pointer.split("/").slice(1).map(decodeJsonPointerSegment);
|
|
2136
|
+
let step: PodSpecPathStep = PodSpecPathStep.PodSpecOrAbove;
|
|
2137
|
+
let isListPosition: boolean = false;
|
|
2138
|
+
|
|
2139
|
+
for (const segment of segments) {
|
|
2140
|
+
isListPosition = isListStep(step) && JSON_PATCH_LIST_POSITION.test(segment);
|
|
2141
|
+
|
|
2142
|
+
const next: PodSpecPathStep | undefined = nextPodSpecPathStep(
|
|
2143
|
+
step,
|
|
2144
|
+
segment,
|
|
2145
|
+
);
|
|
2146
|
+
|
|
2147
|
+
if (next === undefined) {
|
|
2148
|
+
return undefined;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
step = next;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
return { step, isListPosition };
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
function findJsonPatchReplacement(
|
|
2158
|
+
operation: Record<string, unknown>,
|
|
2159
|
+
): string | null {
|
|
2160
|
+
const op: unknown = operation["op"];
|
|
2161
|
+
const path: unknown = operation["path"];
|
|
2162
|
+
const from: unknown = operation["from"];
|
|
2163
|
+
|
|
2164
|
+
// test only compares; add inserts at a list position and replaces a member.
|
|
2165
|
+
if (op === "test") {
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
if (typeof path === "string") {
|
|
2170
|
+
const target: JsonPatchTarget | undefined = readJsonPatchTarget(path);
|
|
2171
|
+
|
|
2172
|
+
if (target && !(op === "add" && target.isListPosition)) {
|
|
2173
|
+
return `kubectl patch with a JSON-patch "${String(op)}" of ${describeJsonPointer(path)} is never allowed for OneUptime AI: ${POD_SPEC_REPLACEMENT_DROPS} (${POD_SPEC_REPLACEMENT_INSTEAD})`;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
if (typeof from === "string" && op !== "add") {
|
|
2178
|
+
if (readJsonPatchTarget(from)) {
|
|
2179
|
+
return `kubectl patch with a JSON-patch "${String(op)}" from ${describeJsonPointer(from)} is never allowed for OneUptime AI: ${POD_SPEC_REPLACEMENT_DROPS} (${POD_SPEC_REPLACEMENT_INSTEAD})`;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
function describeJsonPointer(pointer: string): string {
|
|
2187
|
+
return pointer === "" ? "the whole object" : pointer;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/*
|
|
2191
|
+
* The probe and lifecycle-hook handlers whose `host` the Pod Security
|
|
2192
|
+
* Standards baseline refuses (its Host Probes / Lifecycle Hooks control,
|
|
2193
|
+
* Kubernetes v1.34+): a liveness, readiness or startup probe, or a postStart
|
|
2194
|
+
* or preStop hook, with httpGet.host or tcpSocket.host makes the kubelet
|
|
2195
|
+
* connect from the node to any address it names instead of to the pod. The
|
|
2196
|
+
* name check reads `host` only as a member of one of these (lowercased): a
|
|
2197
|
+
* `host` anywhere else, such as an Ingress rule's, is no pod field.
|
|
2198
|
+
*/
|
|
2199
|
+
const HOST_FIELD_HANDLER_KEYS: Set<string> = new Set<string>([
|
|
2200
|
+
"httpget",
|
|
2201
|
+
"tcpsocket",
|
|
2202
|
+
]);
|
|
2203
|
+
|
|
2204
|
+
const HANDLER_HOST_KEY: string = "host";
|
|
2205
|
+
|
|
2206
|
+
/*
|
|
2207
|
+
* Why a key (or a JSON-pointer segment) may not appear in a patch body, as
|
|
2208
|
+
* the field to name, or null. `parentKey` is the key of the object that
|
|
2209
|
+
* holds it ("" at the top, and for a list element), for the fields that are
|
|
2210
|
+
* forbidden only where they sit (HOST_FIELD_HANDLER_KEYS).
|
|
2211
|
+
*/
|
|
2212
|
+
function forbiddenFieldInName(name: string, parentKey: string): string | null {
|
|
2213
|
+
if (POD_SECURITY_PATCH_KEYS.has(name.toLowerCase())) {
|
|
2214
|
+
return name;
|
|
2215
|
+
}
|
|
2216
|
+
if (isPodSecurityKey(name) || isAppArmorAnnotationKey(name)) {
|
|
2217
|
+
return name;
|
|
2218
|
+
}
|
|
2219
|
+
if (
|
|
2220
|
+
name.toLowerCase() === HANDLER_HOST_KEY &&
|
|
2221
|
+
HOST_FIELD_HANDLER_KEYS.has(parentKey.toLowerCase())
|
|
2222
|
+
) {
|
|
2223
|
+
return `${parentKey}.${name}`;
|
|
2224
|
+
}
|
|
2225
|
+
return null;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// A JSON pointer ("/spec/template/spec/volumes/-"), decoded segment by segment.
|
|
2229
|
+
function jsonPointerSegments(pointer: string): Array<string> {
|
|
2230
|
+
return pointer.split("/").map(decodeJsonPointerSegment);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// A JSON-pointer path ("/spec/template/spec/volumes/-"), segment by segment.
|
|
2234
|
+
function forbiddenFieldInPath(path: string): string | null {
|
|
2235
|
+
const segments: Array<string> = jsonPointerSegments(path);
|
|
2236
|
+
|
|
2237
|
+
for (let index: number = 0; index < segments.length; index++) {
|
|
2238
|
+
const found: string | null = forbiddenFieldInName(
|
|
2239
|
+
segments[index]!,
|
|
2240
|
+
index > 0 ? segments[index - 1]! : "",
|
|
2241
|
+
);
|
|
2242
|
+
if (found) {
|
|
2243
|
+
return found;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
return null;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
/*
|
|
2250
|
+
* The first forbidden field a parsed patch body names (see
|
|
2251
|
+
* forbiddenFieldInName), PATCH_BODY_TOO_DEEP, or null. `parentKey` is the
|
|
2252
|
+
* key that holds `value` — for a JSON-patch operation's value, the last
|
|
2253
|
+
* segment of its path, so `{"op": "add", "path": ".../httpGet", "value":
|
|
2254
|
+
* {"host": ...}}` is read like the same object written in place.
|
|
2255
|
+
*/
|
|
2256
|
+
function findForbiddenJsonField(
|
|
2257
|
+
value: unknown,
|
|
2258
|
+
depth: number,
|
|
2259
|
+
parentKey: string,
|
|
2260
|
+
): string | null {
|
|
2261
|
+
// Nesting this deep is not a patch a fix needs; refuse rather than recurse.
|
|
2262
|
+
if (depth > MAX_PATCH_BODY_DEPTH) {
|
|
2263
|
+
return PATCH_BODY_TOO_DEEP;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (Array.isArray(value)) {
|
|
2267
|
+
for (const item of value) {
|
|
2268
|
+
const found: string | null = findForbiddenJsonField(item, depth + 1, "");
|
|
2269
|
+
if (found) {
|
|
2270
|
+
return found;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
if (value === null || typeof value !== "object") {
|
|
2277
|
+
return null;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
const record: Record<string, unknown> = value as Record<string, unknown>;
|
|
2281
|
+
const isJsonPatchOperation: boolean = Object.prototype.hasOwnProperty.call(
|
|
2282
|
+
record,
|
|
2283
|
+
"op",
|
|
2284
|
+
);
|
|
2285
|
+
const operationPath: unknown = record["path"];
|
|
2286
|
+
const operationValueParent: string =
|
|
2287
|
+
isJsonPatchOperation && typeof operationPath === "string"
|
|
2288
|
+
? jsonPointerSegments(operationPath).pop() || ""
|
|
2289
|
+
: "";
|
|
2290
|
+
|
|
2291
|
+
for (const [key, child] of Object.entries(record)) {
|
|
2292
|
+
const keyProblem: string | null = forbiddenFieldInName(key, parentKey);
|
|
2293
|
+
if (keyProblem) {
|
|
2294
|
+
return keyProblem;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/*
|
|
2298
|
+
* Strategic-merge directives name the list they edit after the slash:
|
|
2299
|
+
* "$setElementOrder/volumes", "$deleteFromPrimitiveList/args".
|
|
2300
|
+
*/
|
|
2301
|
+
if (key.startsWith("$") && key.includes("/")) {
|
|
2302
|
+
const directiveProblem: string | null = forbiddenFieldInName(
|
|
2303
|
+
key.slice(key.indexOf("/") + 1),
|
|
2304
|
+
parentKey,
|
|
2305
|
+
);
|
|
2306
|
+
if (directiveProblem) {
|
|
2307
|
+
return directiveProblem;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (
|
|
2312
|
+
isJsonPatchOperation &&
|
|
2313
|
+
(key === "path" || key === "from") &&
|
|
2314
|
+
typeof child === "string"
|
|
2315
|
+
) {
|
|
2316
|
+
const pathProblem: string | null = forbiddenFieldInPath(child);
|
|
2317
|
+
if (pathProblem) {
|
|
2318
|
+
return pathProblem;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
const found: string | null = findForbiddenJsonField(
|
|
2323
|
+
child,
|
|
2324
|
+
depth + 1,
|
|
2325
|
+
isJsonPatchOperation && key === "value" ? operationValueParent : key,
|
|
2326
|
+
);
|
|
2327
|
+
if (found) {
|
|
2328
|
+
return found;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// Same bounds CommandPolicy applies to the Bash lane's allowlist.
|
|
2336
|
+
const MAX_ALLOWLIST_PATTERNS: number = 100;
|
|
2337
|
+
const MAX_ALLOWLIST_PATTERN_LENGTH: number = 500;
|
|
2338
|
+
|
|
2339
|
+
/*
|
|
2340
|
+
* Exported so the server's save validation and the AI page's form
|
|
2341
|
+
* validation hold entries to exactly the bounds matchesAllowlist reads.
|
|
2342
|
+
*/
|
|
2343
|
+
export const KUBECTL_ALLOWLIST_MAX_PATTERNS: number = MAX_ALLOWLIST_PATTERNS;
|
|
2344
|
+
export const KUBECTL_ALLOWLIST_MAX_PATTERN_LENGTH: number =
|
|
2345
|
+
MAX_ALLOWLIST_PATTERN_LENGTH;
|
|
2346
|
+
|
|
2347
|
+
function isFlagToken(token: string): boolean {
|
|
2348
|
+
return token.startsWith("-") && token !== "-";
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// What tokenize() says for a command (or an allowlist entry) with no verb.
|
|
2352
|
+
const NO_VERB_MESSAGE: string = "The command names no kubectl verb.";
|
|
2353
|
+
|
|
2354
|
+
// A word that holds whitespace was quoted together (see readAllowlistEntry).
|
|
2355
|
+
const WHITESPACE_REGEX: RegExp = /\s/;
|
|
2356
|
+
|
|
2357
|
+
// An allowlist entry as matchesAllowlist reads it (see readAllowlistEntry).
|
|
2358
|
+
interface AllowlistEntry {
|
|
2359
|
+
// The entry's words without the optional leading "kubectl"; set when valid.
|
|
2360
|
+
args?: Array<string> | undefined;
|
|
2361
|
+
// pflag's reading of those words; set when valid.
|
|
2362
|
+
parsed?: ParsedArgs | undefined;
|
|
2363
|
+
// Why the entry is invalid, for the person who typed it; set when invalid.
|
|
2364
|
+
problem?: string | undefined;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/*
|
|
2368
|
+
* Flags whose value picks WHICH objects (or namespace) a command acts on —
|
|
2369
|
+
* the namespace, a selector, --all, the source object of --from — so a `*`
|
|
2370
|
+
* in their value makes an allowlist entry broad.
|
|
2371
|
+
*/
|
|
2372
|
+
const WIDENING_FLAGS: Array<string> = [
|
|
2373
|
+
"n",
|
|
2374
|
+
"namespace",
|
|
2375
|
+
"l",
|
|
2376
|
+
"selector",
|
|
2377
|
+
"field-selector",
|
|
2378
|
+
"pod-selector",
|
|
2379
|
+
"all",
|
|
2380
|
+
"A",
|
|
2381
|
+
"all-namespaces",
|
|
2382
|
+
"from",
|
|
2383
|
+
];
|
|
2384
|
+
|
|
2385
|
+
// For the "write the subcommand out" message.
|
|
2386
|
+
const SUBCOMMAND_EXAMPLES: Record<string, string> = {
|
|
2387
|
+
rollout: "restart deployment/web -n web",
|
|
2388
|
+
set: "image deployment/web * -n web",
|
|
2389
|
+
create: "job manual-run --from=cronjob/nightly -n web",
|
|
2390
|
+
auth: "can-i get pods",
|
|
2391
|
+
top: "pod -n web",
|
|
2392
|
+
};
|
|
2393
|
+
|
|
2394
|
+
function subcommandExample(verb: string): string {
|
|
2395
|
+
const example: string | undefined = Object.prototype.hasOwnProperty.call(
|
|
2396
|
+
SUBCOMMAND_EXAMPLES,
|
|
2397
|
+
verb,
|
|
2398
|
+
)
|
|
2399
|
+
? SUBCOMMAND_EXAMPLES[verb]
|
|
2400
|
+
: undefined;
|
|
2401
|
+
return example ? `, as in "kubectl ${verb} ${example}"` : "";
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function hasWildcard(token: string): boolean {
|
|
2405
|
+
return token.includes("*");
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
/*
|
|
2409
|
+
* Whether evaluateForAutoExecution can ever promote a command this entry
|
|
2410
|
+
* matches through the allowlist: never a read (it runs anyway), and never a
|
|
2411
|
+
* drain, a taint or a patch that names a Node (it always needs a human;
|
|
2412
|
+
* describeAlwaysHumanWrite reads the entry's objects exactly as it reads a
|
|
2413
|
+
* command's, so `kubectl patch node * -p *` promotes nothing). A `*` where
|
|
2414
|
+
* the kind goes may still stand for a Node, but it stands for every other
|
|
2415
|
+
* kind as well, so such an entry is broad, and the Node commands it matches
|
|
2416
|
+
* need a human all the same.
|
|
2417
|
+
*/
|
|
2418
|
+
function allowlistMayPromote(parsed: ParsedArgs): boolean {
|
|
2419
|
+
const verb: string = parsed.positionals[0] || "";
|
|
2420
|
+
const subcommand: string = parsed.positionals[1] || "";
|
|
2421
|
+
|
|
2422
|
+
if (READ_VERBS.has(verb) || READ_COMMAND_GROUPS.has(verb)) {
|
|
2423
|
+
return false;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
if (
|
|
2427
|
+
describeAlwaysHumanWrite(verb, commandTargets(verb, parsed.positionals)) !==
|
|
2428
|
+
undefined
|
|
2429
|
+
) {
|
|
2430
|
+
return false;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
return !(
|
|
2434
|
+
verb === "rollout" &&
|
|
2435
|
+
(subcommand === "status" || subcommand === "history")
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
/*
|
|
2440
|
+
* The commands whose words after the objects are UPDATES kubectl requires
|
|
2441
|
+
* (label, annotate, set image, and set env unless -e/--env/--from supply the
|
|
2442
|
+
* variables): with no update word, kubectl refuses the command outright.
|
|
2443
|
+
*/
|
|
2444
|
+
function readsUpdatePairs(
|
|
2445
|
+
verb: string,
|
|
2446
|
+
subcommand: string,
|
|
2447
|
+
flags: Map<string, Array<string>>,
|
|
2448
|
+
): boolean {
|
|
2449
|
+
if (verb === "label" || verb === "annotate") {
|
|
2450
|
+
return true;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
if (verb !== "set") {
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
if (subcommand === "image") {
|
|
2458
|
+
return true;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
return (
|
|
2462
|
+
subcommand === "env" &&
|
|
2463
|
+
!flags.has("e") &&
|
|
2464
|
+
!flags.has("env") &&
|
|
2465
|
+
!flags.has("from")
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/*
|
|
2470
|
+
* An update word (KEY=VALUE or KEY-) in an allowlist entry: every word it
|
|
2471
|
+
* matches is one too, so it can never stand for another object.
|
|
2472
|
+
*/
|
|
2473
|
+
function isUpdatePairToken(token: string): boolean {
|
|
2474
|
+
return token.includes("=") || (token.endsWith("-") && token !== "-");
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/*
|
|
2478
|
+
* One pattern token against one argv token (see the allowlist section in the
|
|
2479
|
+
* header). A flag is matched only by the same flag spelled literally: the
|
|
2480
|
+
* part up to "=" must be equal, and only a value after "=" may glob. A `*`
|
|
2481
|
+
* never matches whitespace; whitespace runs inside a quoted token must line
|
|
2482
|
+
* up with whitespace in the pattern (runs are collapsed on both sides).
|
|
2483
|
+
*/
|
|
2484
|
+
function allowlistTokenMatches(pattern: string, token: string): boolean {
|
|
2485
|
+
if (isFlagToken(token) || isFlagToken(pattern)) {
|
|
2486
|
+
if (!isFlagToken(token) || !isFlagToken(pattern)) {
|
|
2487
|
+
return false;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
const tokenEq: number = token.indexOf("=");
|
|
2491
|
+
const patternEq: number = pattern.indexOf("=");
|
|
2492
|
+
|
|
2493
|
+
if (tokenEq < 0 || patternEq < 0) {
|
|
2494
|
+
return pattern === token;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
if (pattern.slice(0, patternEq) !== token.slice(0, tokenEq)) {
|
|
2498
|
+
return false;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
return globWithinToken(
|
|
2502
|
+
pattern.slice(patternEq + 1),
|
|
2503
|
+
token.slice(tokenEq + 1),
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
return globWithinToken(pattern, token);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function globWithinToken(pattern: string, text: string): boolean {
|
|
2511
|
+
const patternPieces: Array<string> = pattern.trim().split(/\s+/);
|
|
2512
|
+
const textPieces: Array<string> = text.trim().split(/\s+/);
|
|
2513
|
+
|
|
2514
|
+
if (patternPieces.length !== textPieces.length) {
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
for (let i: number = 0; i < patternPieces.length; i++) {
|
|
2519
|
+
if (!CommandPolicy.globMatches(patternPieces[i]!, textPieces[i]!)) {
|
|
2520
|
+
return false;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
return true;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// A replica count kubectl scale takes: digits only.
|
|
2528
|
+
const REPLICA_COUNT_PATTERN: RegExp = /^[0-9]+$/;
|
|
2529
|
+
|
|
2530
|
+
export default class KubectlPolicy {
|
|
2531
|
+
/*
|
|
2532
|
+
* Split a command line into an argv the way a POSIX shell would split it
|
|
2533
|
+
* for quoting purposes ONLY: single quotes, double quotes, backslash
|
|
2534
|
+
* escapes. No expansion, no operators, no redirection — those characters
|
|
2535
|
+
* stay literal inside their token. The command must fit on one line.
|
|
2536
|
+
*/
|
|
2537
|
+
public static tokenize(command: string): KubectlTokenizeResult {
|
|
2538
|
+
const text: string = (command || "").trim();
|
|
2539
|
+
|
|
2540
|
+
if (!text) {
|
|
2541
|
+
return { errorMessage: "Empty command." };
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
if (text.length > MAX_COMMAND_LENGTH_CHARS) {
|
|
2545
|
+
return {
|
|
2546
|
+
errorMessage: `Command exceeds the ${MAX_COMMAND_LENGTH_CHARS}-character limit.`,
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
if (text.includes("\n") || text.includes("\r") || text.includes("\0")) {
|
|
2551
|
+
return { errorMessage: "A kubectl command must be a single line." };
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
const args: Array<string> = [];
|
|
2555
|
+
let current: string = "";
|
|
2556
|
+
let inSingle: boolean = false;
|
|
2557
|
+
let inDouble: boolean = false;
|
|
2558
|
+
let hasToken: boolean = false;
|
|
2559
|
+
|
|
2560
|
+
for (let i: number = 0; i < text.length; i++) {
|
|
2561
|
+
const ch: string = text[i]!;
|
|
2562
|
+
|
|
2563
|
+
if (inSingle) {
|
|
2564
|
+
if (ch === "'") {
|
|
2565
|
+
inSingle = false;
|
|
2566
|
+
} else {
|
|
2567
|
+
current += ch;
|
|
2568
|
+
}
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
if (inDouble) {
|
|
2573
|
+
if (ch === '"') {
|
|
2574
|
+
inDouble = false;
|
|
2575
|
+
} else if (ch === "\\" && i + 1 < text.length) {
|
|
2576
|
+
const next: string = text[i + 1]!;
|
|
2577
|
+
if (next === '"' || next === "\\") {
|
|
2578
|
+
current += next;
|
|
2579
|
+
i++;
|
|
2580
|
+
} else {
|
|
2581
|
+
current += ch;
|
|
2582
|
+
}
|
|
2583
|
+
} else {
|
|
2584
|
+
current += ch;
|
|
2585
|
+
}
|
|
2586
|
+
continue;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
if (ch === "'") {
|
|
2590
|
+
inSingle = true;
|
|
2591
|
+
hasToken = true;
|
|
2592
|
+
continue;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
if (ch === '"') {
|
|
2596
|
+
inDouble = true;
|
|
2597
|
+
hasToken = true;
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
if (ch === "\\" && i + 1 < text.length) {
|
|
2602
|
+
current += text[i + 1];
|
|
2603
|
+
hasToken = true;
|
|
2604
|
+
i++;
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
if (ch === " " || ch === "\t") {
|
|
2609
|
+
if (hasToken) {
|
|
2610
|
+
args.push(current);
|
|
2611
|
+
current = "";
|
|
2612
|
+
hasToken = false;
|
|
2613
|
+
}
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
current += ch;
|
|
2618
|
+
hasToken = true;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
if (inSingle || inDouble) {
|
|
2622
|
+
return { errorMessage: "Unbalanced quotes in the command." };
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
if (hasToken) {
|
|
2626
|
+
args.push(current);
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
if (args.length === 0) {
|
|
2630
|
+
return { errorMessage: "Empty command." };
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
if (args.length > MAX_TOKENS) {
|
|
2634
|
+
return {
|
|
2635
|
+
errorMessage: `A kubectl command may have at most ${MAX_TOKENS} arguments.`,
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// The model may or may not write the binary name; the Runner adds it.
|
|
2640
|
+
if (args[0]!.toLowerCase() === "kubectl") {
|
|
2641
|
+
args.shift();
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
if (args.length === 0) {
|
|
2645
|
+
return { errorMessage: NO_VERB_MESSAGE };
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
return { args };
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// Tokenize, then evaluate.
|
|
2652
|
+
public static evaluateCommand(command: string): KubectlPolicyResult {
|
|
2653
|
+
const tokenized: KubectlTokenizeResult = KubectlPolicy.tokenize(command);
|
|
2654
|
+
|
|
2655
|
+
if (!tokenized.args) {
|
|
2656
|
+
return {
|
|
2657
|
+
tier: KubectlCommandTier.Denied,
|
|
2658
|
+
reason: tokenized.errorMessage || "Could not parse the command.",
|
|
2659
|
+
args: [],
|
|
2660
|
+
verb: "",
|
|
2661
|
+
displayCommand: (command || "").trim().slice(0, 200),
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
return KubectlPolicy.evaluateArgs(tokenized.args);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/*
|
|
2669
|
+
* The evaluation proper, on an argv. The Runner calls this on the argv it
|
|
2670
|
+
* received so its verdict never depends on re-tokenizing a string the
|
|
2671
|
+
* same way the server did.
|
|
2672
|
+
*/
|
|
2673
|
+
public static evaluateArgs(rawArgs: Array<string>): KubectlPolicyResult {
|
|
2674
|
+
const args: Array<string> = (rawArgs || []).filter((arg: unknown) => {
|
|
2675
|
+
return typeof arg === "string";
|
|
2676
|
+
});
|
|
2677
|
+
|
|
2678
|
+
const displayCommand: string = KubectlPolicy.renderDisplayCommand(args);
|
|
2679
|
+
|
|
2680
|
+
const deny: (reason: string, verb?: string) => KubectlPolicyResult = (
|
|
2681
|
+
reason: string,
|
|
2682
|
+
verb: string = "",
|
|
2683
|
+
): KubectlPolicyResult => {
|
|
2684
|
+
return {
|
|
2685
|
+
tier: KubectlCommandTier.Denied,
|
|
2686
|
+
reason,
|
|
2687
|
+
args,
|
|
2688
|
+
verb,
|
|
2689
|
+
displayCommand,
|
|
2690
|
+
};
|
|
2691
|
+
};
|
|
2692
|
+
|
|
2693
|
+
if (args.length === 0) {
|
|
2694
|
+
return deny(NO_VERB_MESSAGE);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
if (args.length > MAX_TOKENS) {
|
|
2698
|
+
return deny(
|
|
2699
|
+
`A kubectl command may have at most ${MAX_TOKENS} arguments.`,
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
for (const arg of args) {
|
|
2704
|
+
if (arg.includes("\n") || arg.includes("\r") || arg.includes("\0")) {
|
|
2705
|
+
return deny("Arguments may not contain line breaks.");
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
const parsed: ParsedArgs = KubectlPolicy.parseArgs(args);
|
|
2710
|
+
|
|
2711
|
+
const rawVerb: string = parsed.positionals[0] || "";
|
|
2712
|
+
const verb: string = rawVerb.toLowerCase();
|
|
2713
|
+
|
|
2714
|
+
// A denied verb is the most useful thing to say, whatever its flags.
|
|
2715
|
+
if (DENIED_VERBS.has(verb)) {
|
|
2716
|
+
return deny(`kubectl ${verb} is not allowed for OneUptime AI`, verb);
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
// Credential / cluster-selection / file / unknown flags: never.
|
|
2720
|
+
if (parsed.violation) {
|
|
2721
|
+
return deny(parsed.violation);
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
if (!verb) {
|
|
2725
|
+
return deny(NO_VERB_MESSAGE);
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
/*
|
|
2729
|
+
* cobra matches commands case-sensitively: "Get" is not the get command,
|
|
2730
|
+
* and kubectl would look for a kubectl-Get plugin on the Runner's PATH
|
|
2731
|
+
* and run it. The same holds one level down for subcommands.
|
|
2732
|
+
*/
|
|
2733
|
+
if (rawVerb !== verb) {
|
|
2734
|
+
return deny(
|
|
2735
|
+
`kubectl matches commands case-sensitively, so "${rawVerb}" is not kubectl ${verb}: kubectl would look for a kubectl-${rawVerb} plugin on the Runner (write the verb in lowercase)`,
|
|
2736
|
+
verb,
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
const rawSubcommand: string = parsed.positionals[1] || "";
|
|
2741
|
+
const subcommand: string = rawSubcommand.toLowerCase();
|
|
2742
|
+
|
|
2743
|
+
if (SUBCOMMAND_VERBS.has(verb) && rawSubcommand !== subcommand) {
|
|
2744
|
+
return deny(
|
|
2745
|
+
`kubectl matches commands case-sensitively, so "${rawSubcommand}" is not the kubectl ${verb} ${subcommand} command (write the subcommand in lowercase)`,
|
|
2746
|
+
`${verb} ${subcommand}`,
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
const streamingProblem: string | null = KubectlPolicy.findStreamingFlag(
|
|
2751
|
+
parsed.flags,
|
|
2752
|
+
);
|
|
2753
|
+
|
|
2754
|
+
if (streamingProblem) {
|
|
2755
|
+
return deny(streamingProblem);
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
const outputProblem: string | null = KubectlPolicy.findOutputFormatProblem(
|
|
2759
|
+
parsed.flags,
|
|
2760
|
+
);
|
|
2761
|
+
|
|
2762
|
+
if (outputProblem) {
|
|
2763
|
+
return deny(outputProblem);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
/*
|
|
2767
|
+
* The namespace, resolved from -n / --namespace in every spelling
|
|
2768
|
+
* parseArgs accepts (-n X, -nX, -n=X, --namespace X, --namespace=X).
|
|
2769
|
+
* pflag keeps the LAST value of a repeated flag, so `-n web ... -n
|
|
2770
|
+
* kube-system` would run in kube-system while a reader (or an allowlist
|
|
2771
|
+
* pattern) saw web first: a namespace set twice is refused outright.
|
|
2772
|
+
*/
|
|
2773
|
+
const namespaceValues: Array<string> = [
|
|
2774
|
+
...(parsed.flags.get("n") || []),
|
|
2775
|
+
...(parsed.flags.get("namespace") || []),
|
|
2776
|
+
];
|
|
2777
|
+
|
|
2778
|
+
if (namespaceValues.length > 1) {
|
|
2779
|
+
return deny(
|
|
2780
|
+
`the namespace is set more than once (${namespaceValues
|
|
2781
|
+
.map((value: string) => {
|
|
2782
|
+
return `"${value}"`;
|
|
2783
|
+
})
|
|
2784
|
+
.join(", ")}); kubectl silently uses the last one, so set -n once`,
|
|
2785
|
+
verb,
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
const namespace: string | undefined = namespaceValues[0];
|
|
2790
|
+
|
|
2791
|
+
const hasAllNamespaces: boolean =
|
|
2792
|
+
parsed.flags.has("A") || parsed.flags.has("all-namespaces");
|
|
2793
|
+
const hasAll: boolean = parsed.flags.has("all");
|
|
2794
|
+
/*
|
|
2795
|
+
* A field selector selects exactly like a label selector (kubectl's
|
|
2796
|
+
* builder visits every match), so for a write both are "a selector".
|
|
2797
|
+
*/
|
|
2798
|
+
const hasSelector: boolean =
|
|
2799
|
+
parsed.flags.has("l") ||
|
|
2800
|
+
parsed.flags.has("selector") ||
|
|
2801
|
+
parsed.flags.has("field-selector");
|
|
2802
|
+
const hasForce: boolean = parsed.flags.has("force");
|
|
2803
|
+
|
|
2804
|
+
// The objects a write names (see commandTargets).
|
|
2805
|
+
const targets: NamedKinds = commandTargets(verb, parsed.positionals);
|
|
2806
|
+
|
|
2807
|
+
/*
|
|
2808
|
+
* Set for a drain, a taint and a patch of a Node: the write carries
|
|
2809
|
+
* requiresHuman, whatever tier it gets (see the header).
|
|
2810
|
+
*/
|
|
2811
|
+
const alwaysHumanReason: string | undefined = describeAlwaysHumanWrite(
|
|
2812
|
+
verb,
|
|
2813
|
+
targets,
|
|
2814
|
+
);
|
|
2815
|
+
|
|
2816
|
+
const createKind: string | undefined =
|
|
2817
|
+
verb === "create" &&
|
|
2818
|
+
Object.prototype.hasOwnProperty.call(CREATE_SUBCOMMANDS, rawSubcommand)
|
|
2819
|
+
? CREATE_SUBCOMMANDS[rawSubcommand]
|
|
2820
|
+
: undefined;
|
|
2821
|
+
|
|
2822
|
+
/*
|
|
2823
|
+
* Where a write lands, for the protected-namespace rule. Nodes and the
|
|
2824
|
+
* other built-in cluster-scoped kinds have no namespace (kubectl ignores
|
|
2825
|
+
* -n for them); what could be protected there is a Namespace object
|
|
2826
|
+
* itself. Only the built-in kinds in their own API group count
|
|
2827
|
+
* (builtinKind()): a custom resource that borrows a built-in name —
|
|
2828
|
+
* `annotate nodes.example.com x -n kube-system` — has a scope this
|
|
2829
|
+
* policy cannot see, so it is judged by the namespace -n names.
|
|
2830
|
+
*/
|
|
2831
|
+
const isClusterScopedWrite: boolean =
|
|
2832
|
+
(NODE_NAME_VERBS.has(verb) &&
|
|
2833
|
+
namesOnlyNodes(parsed.positionals.slice(1))) ||
|
|
2834
|
+
(createKind !== undefined &&
|
|
2835
|
+
CLUSTER_SCOPED_CREATE_KINDS.has(createKind)) ||
|
|
2836
|
+
everyBuiltinKindIn(targets, CLUSTER_SCOPED_KINDS);
|
|
2837
|
+
|
|
2838
|
+
let protectedNamespace: string | undefined = undefined;
|
|
2839
|
+
|
|
2840
|
+
/*
|
|
2841
|
+
* A Namespace object named after a protected namespace is protected
|
|
2842
|
+
* whatever -n says. That test reads the folded kind (normalizeKind), so
|
|
2843
|
+
* a custom resource that borrows the name `namespaces` and is NAMED
|
|
2844
|
+
* kube-system needs a human too: the folded reading can only add a
|
|
2845
|
+
* human, never remove one.
|
|
2846
|
+
*/
|
|
2847
|
+
if (!isClusterScopedWrite && isProtectedKubernetesNamespace(namespace)) {
|
|
2848
|
+
protectedNamespace = namespace!.trim().toLowerCase();
|
|
2849
|
+
} else if (targets.kinds.has("namespace")) {
|
|
2850
|
+
const protectedObject: string | undefined = targets.names.find(
|
|
2851
|
+
(name: string) => {
|
|
2852
|
+
return isProtectedKubernetesNamespace(name);
|
|
2853
|
+
},
|
|
2854
|
+
);
|
|
2855
|
+
protectedNamespace = protectedObject
|
|
2856
|
+
? protectedObject.trim().toLowerCase()
|
|
2857
|
+
: undefined;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
/*
|
|
2861
|
+
* The result for a command that may run. A write that lands in (or
|
|
2862
|
+
* targets) a protected namespace is lifted from SafeWrite to RiskyWrite
|
|
2863
|
+
* and marked, and a drain, a taint or a patch of a Node is marked too,
|
|
2864
|
+
* so evaluateForAutoExecution never runs either unattended.
|
|
2865
|
+
*/
|
|
2866
|
+
const allowed: (
|
|
2867
|
+
tier: KubectlCommandTier,
|
|
2868
|
+
reason: string,
|
|
2869
|
+
verbLabel: string,
|
|
2870
|
+
) => KubectlPolicyResult = (
|
|
2871
|
+
tier: KubectlCommandTier,
|
|
2872
|
+
reason: string,
|
|
2873
|
+
verbLabel: string,
|
|
2874
|
+
): KubectlPolicyResult => {
|
|
2875
|
+
const result: KubectlPolicyResult = {
|
|
2876
|
+
tier,
|
|
2877
|
+
reason,
|
|
2878
|
+
args,
|
|
2879
|
+
verb: verbLabel,
|
|
2880
|
+
displayCommand,
|
|
2881
|
+
};
|
|
2882
|
+
|
|
2883
|
+
if (namespace !== undefined) {
|
|
2884
|
+
result.namespace = namespace;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
if (tier === KubectlCommandTier.Read) {
|
|
2888
|
+
return result;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
if (alwaysHumanReason !== undefined) {
|
|
2892
|
+
result.requiresHuman = true;
|
|
2893
|
+
result.reason = alwaysHumanReason;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
if (!protectedNamespace) {
|
|
2897
|
+
return result;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
result.protectedNamespace = protectedNamespace;
|
|
2901
|
+
|
|
2902
|
+
/*
|
|
2903
|
+
* Built on result.reason, so a write that needs a human for both
|
|
2904
|
+
* reasons (a patch of a Node and of a workload in kube-system) says
|
|
2905
|
+
* both.
|
|
2906
|
+
*/
|
|
2907
|
+
if (tier === KubectlCommandTier.SafeWrite) {
|
|
2908
|
+
result.tier = KubectlCommandTier.RiskyWrite;
|
|
2909
|
+
result.reason = `${result.reason}, but it changes the protected namespace ${protectedNamespace}, so a human must approve it`;
|
|
2910
|
+
} else {
|
|
2911
|
+
result.reason = `${result.reason}; it changes the protected namespace ${protectedNamespace}, which always needs a human`;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
return result;
|
|
2915
|
+
};
|
|
2916
|
+
|
|
2917
|
+
// Kinds for messages: normalized for labels, as written for "not a ...".
|
|
2918
|
+
const kindList: string = Array.from(targets.kinds).join(",");
|
|
2919
|
+
const writtenKindList: string = targets.rawKinds.join(",");
|
|
2920
|
+
|
|
2921
|
+
// ---- Credentials -----------------------------------------------------
|
|
2922
|
+
|
|
2923
|
+
if (OBJECT_VERBS.has(verb)) {
|
|
2924
|
+
for (const kind of targets.kinds) {
|
|
2925
|
+
if (CREDENTIAL_KINDS.has(kind)) {
|
|
2926
|
+
return deny(
|
|
2927
|
+
`kubectl ${verb} on Secret objects is never allowed for OneUptime AI: it never reads, changes or deletes Secrets, whose values would otherwise reach the model and the job record (describe the workload that uses the Secret instead)`,
|
|
2928
|
+
`${verb} ${kind}`,
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
if (verb === "create") {
|
|
2935
|
+
// `create token` mints a ServiceAccount token and prints it.
|
|
2936
|
+
if (subcommand === "token") {
|
|
2937
|
+
return deny(
|
|
2938
|
+
"kubectl create token mints a credential and prints it; OneUptime AI never handles credentials",
|
|
2939
|
+
"create token",
|
|
2940
|
+
);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
if (normalizeKind(subcommand) === "secret") {
|
|
2944
|
+
return deny(
|
|
2945
|
+
"kubectl create secret is never allowed for OneUptime AI: it would put credential values in the command, the approval card and the job record (OneUptime AI never handles credentials)",
|
|
2946
|
+
"create secret",
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
const deniedCreateKind: string | undefined =
|
|
2951
|
+
createKind && DENIED_CREATE_KINDS.has(createKind)
|
|
2952
|
+
? createKind
|
|
2953
|
+
: RBAC_KINDS.has(normalizeKind(subcommand))
|
|
2954
|
+
? normalizeKind(subcommand)
|
|
2955
|
+
: undefined;
|
|
2956
|
+
|
|
2957
|
+
if (deniedCreateKind) {
|
|
2958
|
+
return deny(
|
|
2959
|
+
`kubectl create ${subcommand} is never allowed for OneUptime AI: roles, role bindings and ServiceAccounts decide who may do what in the cluster, and creating one is a privilege grant, not a fix (a human makes RBAC changes)`,
|
|
2960
|
+
`create ${deniedCreateKind}`,
|
|
2961
|
+
);
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
if (!createKind) {
|
|
2965
|
+
return deny(
|
|
2966
|
+
`kubectl create ${rawSubcommand || "(missing subcommand)"} is not a kubectl create subcommand OneUptime AI may run (for a word it does not know, kubectl would look for a kubectl-create-${rawSubcommand || "<name>"} plugin on the Runner and run it)`,
|
|
2967
|
+
`create ${subcommand}`,
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
if (IMAGE_WORKLOAD_CREATE_KINDS.has(createKind)) {
|
|
2972
|
+
return deny(
|
|
2973
|
+
`kubectl create ${createKind} starts a new workload running the image named in its --image flag (kubectl requires one): a new program in the cluster, not a fix. OneUptime AI changes existing workloads (set image, rollout undo) and re-runs an existing CronJob with kubectl create job NAME --from=cronjob/NAME`,
|
|
2974
|
+
`create ${createKind}`,
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
if (createKind === "job") {
|
|
2979
|
+
if (parsed.flags.has("image")) {
|
|
2980
|
+
return deny(
|
|
2981
|
+
"kubectl create job --image runs an arbitrary image in the cluster; OneUptime AI only re-runs an existing CronJob: kubectl create job NAME --from=cronjob/NAME",
|
|
2982
|
+
"create job",
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
const fromValues: Array<string> = parsed.flags.get("from") || [];
|
|
2987
|
+
const from: string = fromValues.length === 1 ? fromValues[0]! : "";
|
|
2988
|
+
const slash: number = from.indexOf("/");
|
|
2989
|
+
const fromName: string = slash > 0 ? from.slice(slash + 1) : "";
|
|
2990
|
+
const isCronJobRef: boolean =
|
|
2991
|
+
slash > 0 &&
|
|
2992
|
+
builtinKind(from.slice(0, slash)) === "cronjob" &&
|
|
2993
|
+
fromName.length > 0 &&
|
|
2994
|
+
!fromName.includes("/");
|
|
2995
|
+
|
|
2996
|
+
if (!isCronJobRef) {
|
|
2997
|
+
return deny(
|
|
2998
|
+
"kubectl create job may only re-run an existing CronJob: kubectl create job NAME --from=cronjob/NAME",
|
|
2999
|
+
"create job",
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
if (parsed.positionals.length > 3) {
|
|
3004
|
+
return deny(
|
|
3005
|
+
"kubectl create job --from=cronjob/NAME takes one job NAME and no command (the CronJob's own spec runs)",
|
|
3006
|
+
"create job",
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
if (verb === "set") {
|
|
3013
|
+
if (subcommand === "serviceaccount" || subcommand === "sa") {
|
|
3014
|
+
return deny(
|
|
3015
|
+
"kubectl set serviceaccount is never allowed for OneUptime AI: it changes the identity a workload's pods run as, and with it every permission and Secret they can reach",
|
|
3016
|
+
"set serviceaccount",
|
|
3017
|
+
);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
if (subcommand === "subject") {
|
|
3021
|
+
return deny(
|
|
3022
|
+
"kubectl set subject is never allowed for OneUptime AI: adding users, groups or ServiceAccounts to a role binding is a privilege grant, not a fix",
|
|
3023
|
+
"set subject",
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
if (!SET_SUBCOMMANDS.has(subcommand)) {
|
|
3028
|
+
return deny(
|
|
3029
|
+
`kubectl set ${rawSubcommand || "(missing subcommand)"} is not allowed`,
|
|
3030
|
+
`set ${subcommand}`,
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
if (subcommand === "env") {
|
|
3035
|
+
for (const from of parsed.flags.get("from") || []) {
|
|
3036
|
+
if (CREDENTIAL_KINDS.has(normalizeKind(from.split("/")[0] || ""))) {
|
|
3037
|
+
return deny(
|
|
3038
|
+
"kubectl set env --from=secret/... wires a Secret's keys into the workload's environment; OneUptime AI never attaches Secrets to workloads (set plain values, or --from=configmap/...)",
|
|
3039
|
+
"set env",
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// ---- Read tier -------------------------------------------------------
|
|
3047
|
+
|
|
3048
|
+
if (READ_VERBS.has(verb)) {
|
|
3049
|
+
return allowed(
|
|
3050
|
+
KubectlCommandTier.Read,
|
|
3051
|
+
`kubectl ${verb} only reads the cluster`,
|
|
3052
|
+
verb,
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
if (verb === "cluster-info") {
|
|
3057
|
+
if (subcommand === "dump") {
|
|
3058
|
+
return deny(
|
|
3059
|
+
"kubectl cluster-info dump exports the whole cluster; inspect specific resources instead",
|
|
3060
|
+
"cluster-info dump",
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
return allowed(
|
|
3064
|
+
KubectlCommandTier.Read,
|
|
3065
|
+
"kubectl cluster-info only reads the cluster",
|
|
3066
|
+
verb,
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
if (verb === "auth") {
|
|
3071
|
+
if (subcommand === "can-i" || subcommand === "whoami") {
|
|
3072
|
+
return allowed(
|
|
3073
|
+
KubectlCommandTier.Read,
|
|
3074
|
+
`kubectl auth ${subcommand} only reads the cluster`,
|
|
3075
|
+
`auth ${subcommand}`,
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
return deny(
|
|
3079
|
+
`kubectl auth ${subcommand || "(missing subcommand)"} is not allowed`,
|
|
3080
|
+
`auth ${subcommand}`,
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
if (
|
|
3085
|
+
verb === "rollout" &&
|
|
3086
|
+
(subcommand === "status" || subcommand === "history")
|
|
3087
|
+
) {
|
|
3088
|
+
return allowed(
|
|
3089
|
+
KubectlCommandTier.Read,
|
|
3090
|
+
`kubectl rollout ${subcommand} only reads the cluster`,
|
|
3091
|
+
`rollout ${subcommand}`,
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// ---- Write tiers -----------------------------------------------------
|
|
3096
|
+
|
|
3097
|
+
/*
|
|
3098
|
+
* --overrides decides what kubectl expose creates, so the command no
|
|
3099
|
+
* longer says it (see the header and OBJECT_OVERRIDE_FLAGS).
|
|
3100
|
+
*/
|
|
3101
|
+
const overrideFlag: string | undefined = OBJECT_OVERRIDE_FLAGS.find(
|
|
3102
|
+
(flag: string) => {
|
|
3103
|
+
return parsed.flags.has(flag);
|
|
3104
|
+
},
|
|
3105
|
+
);
|
|
3106
|
+
|
|
3107
|
+
if (overrideFlag !== undefined) {
|
|
3108
|
+
const label: string = SUBCOMMAND_VERBS.has(verb)
|
|
3109
|
+
? `${verb} ${subcommand}`
|
|
3110
|
+
: verb;
|
|
3111
|
+
return deny(
|
|
3112
|
+
`kubectl ${label} with --${overrideFlag} is never allowed for OneUptime AI: --overrides (merged as --override-type says) replaces the object kubectl expose generates with whatever its JSON describes, of any kind and in any namespace (a ClusterRoleBinding, a PriorityClass, a Job running any image), so the command no longer says what it creates (write the Service kubectl expose makes with its own flags: --port, --target-port, --name, --type, --protocol)`,
|
|
3113
|
+
label,
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
if (hasAllNamespaces) {
|
|
3118
|
+
const label: string = verb === "rollout" ? `rollout ${subcommand}` : verb;
|
|
3119
|
+
return deny(
|
|
3120
|
+
`kubectl ${label} across all namespaces is not allowed`,
|
|
3121
|
+
label,
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
/*
|
|
3126
|
+
* A ClusterRole aggregation label names the grant it makes, so the
|
|
3127
|
+
* refusal names it too. Aggregation reads labels only; the same key as
|
|
3128
|
+
* an annotation is refused as a plain RBAC write below.
|
|
3129
|
+
*/
|
|
3130
|
+
const clusterRoleAggregationKey: string | undefined =
|
|
3131
|
+
verb === "label" && targets.kinds.has("clusterrole")
|
|
3132
|
+
? labelKeys(verb, parsed.positionals).find(isRbacAggregationKey)
|
|
3133
|
+
: undefined;
|
|
3134
|
+
|
|
3135
|
+
if (OBJECT_VERBS.has(verb)) {
|
|
3136
|
+
for (const kind of targets.kinds) {
|
|
3137
|
+
if (RESOURCE_CATEGORIES.has(kind)) {
|
|
3138
|
+
return deny(
|
|
3139
|
+
`kubectl ${verb} on the resource category "${kind}" is never allowed for OneUptime AI: kubectl expands a category into every kind in it (api-extensions alone covers CRDs, APIServices and admission webhooks), so name the kind instead`,
|
|
3140
|
+
`${verb} ${kind}`,
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
// Deleting these is refused by NEVER_DELETE_KINDS, with its own reason.
|
|
3145
|
+
if (verb === "delete") {
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
if (RBAC_KINDS.has(kind)) {
|
|
3150
|
+
if (clusterRoleAggregationKey) {
|
|
3151
|
+
return deny(
|
|
3152
|
+
`kubectl ${verb} of ${clusterRoleAggregationKey} on a ClusterRole is never allowed for OneUptime AI: it merges the role's rules into admin/edit/view and so grants them to everyone bound to those, a privilege grant, not a fix`,
|
|
3153
|
+
`${verb} clusterrole`,
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
return deny(
|
|
3158
|
+
`kubectl ${verb} on ${kind} objects is never allowed for OneUptime AI: roles and role bindings decide who may do what in the cluster, and changing them is a privilege grant, not a fix (a human makes RBAC changes)`,
|
|
3159
|
+
`${verb} ${kind}`,
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
if (ADMISSION_AND_API_EXTENSION_KINDS.has(kind)) {
|
|
3164
|
+
return deny(
|
|
3165
|
+
`kubectl ${verb} on ${kind} objects is never allowed for OneUptime AI: admission webhooks and policies, APIServices and CRDs decide what the API server admits and serves for the whole cluster, and changing one can switch a cluster control off as surely as deleting it (a human makes that change)`,
|
|
3166
|
+
`${verb} ${kind}`,
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// Pod Security Admission's namespace labels, whichever namespaces they reach.
|
|
3173
|
+
if (
|
|
3174
|
+
(verb === "label" || verb === "annotate") &&
|
|
3175
|
+
targets.kinds.has("namespace")
|
|
3176
|
+
) {
|
|
3177
|
+
const podSecurityKey: string | undefined = labelKeys(
|
|
3178
|
+
verb,
|
|
3179
|
+
parsed.positionals,
|
|
3180
|
+
).find(isPodSecurityKey);
|
|
3181
|
+
|
|
3182
|
+
if (podSecurityKey) {
|
|
3183
|
+
return deny(
|
|
3184
|
+
`kubectl ${verb} of ${podSecurityKey} on a namespace is never allowed for OneUptime AI: pod-security.kubernetes.io keys are Pod Security Admission's controls, which decide whether privileged pods may run there`,
|
|
3185
|
+
`${verb} namespace`,
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
/*
|
|
3191
|
+
* A write to Namespace objects must name each one. A selector, --all or
|
|
3192
|
+
* a bare kind reaches Namespace objects the command never names —
|
|
3193
|
+
* kube-system's among them — so the protected-namespace rule, which
|
|
3194
|
+
* reads the names, could not see it (see the header). Deleting a
|
|
3195
|
+
* Namespace is refused outright above (NEVER_DELETE_KINDS). Only the
|
|
3196
|
+
* built-in Namespace kind counts (builtinKind()): a custom resource that
|
|
3197
|
+
* borrows the name is judged by the namespace -n names.
|
|
3198
|
+
*/
|
|
3199
|
+
if (
|
|
3200
|
+
verb !== "delete" &&
|
|
3201
|
+
targets.rawKinds.some((rawKind: string) => {
|
|
3202
|
+
return builtinKind(rawKind) === "namespace";
|
|
3203
|
+
}) &&
|
|
3204
|
+
(hasSelector || hasAll || targets.namedCount === 0)
|
|
3205
|
+
) {
|
|
3206
|
+
return deny(
|
|
3207
|
+
`kubectl ${verb} of Namespace objects ${
|
|
3208
|
+
hasSelector || hasAll
|
|
3209
|
+
? "by a selector or --all"
|
|
3210
|
+
: "without naming any"
|
|
3211
|
+
} is never allowed for OneUptime AI: it can change kube-system's Namespace object (or another protected one) without naming it, so name each Namespace object (namespace/NAME)`,
|
|
3212
|
+
`${verb} namespace`,
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
if (verb === "rollout") {
|
|
3217
|
+
const rolloutVerb: string = `rollout ${subcommand}`;
|
|
3218
|
+
|
|
3219
|
+
if (
|
|
3220
|
+
subcommand !== "restart" &&
|
|
3221
|
+
subcommand !== "undo" &&
|
|
3222
|
+
subcommand !== "pause" &&
|
|
3223
|
+
subcommand !== "resume"
|
|
3224
|
+
) {
|
|
3225
|
+
return deny(
|
|
3226
|
+
`kubectl rollout ${subcommand || "(missing subcommand)"} is not allowed`,
|
|
3227
|
+
rolloutVerb,
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
if (hasSelector || hasAll) {
|
|
3232
|
+
return allowed(
|
|
3233
|
+
KubectlCommandTier.RiskyWrite,
|
|
3234
|
+
`kubectl ${rolloutVerb} with a selector or --all touches many workloads at once`,
|
|
3235
|
+
rolloutVerb,
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
if (targets.namedCount === 0) {
|
|
3240
|
+
return allowed(
|
|
3241
|
+
KubectlCommandTier.RiskyWrite,
|
|
3242
|
+
`kubectl ${rolloutVerb} names no workload, and kubectl reads a bare kind as every ${kindList || "workload"} in the namespace (name one: TYPE/NAME)`,
|
|
3243
|
+
rolloutVerb,
|
|
3244
|
+
);
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
if (!isOneNamedObject(targets)) {
|
|
3248
|
+
return allowed(
|
|
3249
|
+
KubectlCommandTier.RiskyWrite,
|
|
3250
|
+
`kubectl ${rolloutVerb} names several workloads at once`,
|
|
3251
|
+
rolloutVerb,
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
if (!everyBuiltinKindIn(targets, ROLLOUT_SAFE_KINDS)) {
|
|
3256
|
+
return allowed(
|
|
3257
|
+
KubectlCommandTier.RiskyWrite,
|
|
3258
|
+
`kubectl ${rolloutVerb} on ${writtenKindList} is not a Deployment, StatefulSet or DaemonSet rollout`,
|
|
3259
|
+
rolloutVerb,
|
|
3260
|
+
);
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
return allowed(
|
|
3264
|
+
KubectlCommandTier.SafeWrite,
|
|
3265
|
+
`kubectl ${rolloutVerb} of one named workload is a controller-managed, reversible change`,
|
|
3266
|
+
rolloutVerb,
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
if (verb === "delete") {
|
|
3271
|
+
if (hasAll) {
|
|
3272
|
+
return deny("kubectl delete --all is not allowed", "delete");
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
if (parsed.positionals.length < 2) {
|
|
3276
|
+
return deny("kubectl delete needs a resource kind and name", "delete");
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
for (const kind of targets.kinds) {
|
|
3280
|
+
if (NEVER_DELETE_KINDS.has(kind)) {
|
|
3281
|
+
return deny(
|
|
3282
|
+
`deleting ${kind} objects is never allowed for OneUptime AI`,
|
|
3283
|
+
"delete " + kind,
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
const verbLabel: string = `delete ${kindList}`;
|
|
3289
|
+
|
|
3290
|
+
if (targets.namedCount === 0 && !hasSelector) {
|
|
3291
|
+
return deny(
|
|
3292
|
+
"kubectl delete needs the name of the object to delete (or a selector, which needs approval)",
|
|
3293
|
+
verbLabel,
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
if (
|
|
3298
|
+
!hasSelector &&
|
|
3299
|
+
!hasForce &&
|
|
3300
|
+
isOneNamedObject(targets) &&
|
|
3301
|
+
everyBuiltinKindIn(targets, SAFE_DELETE_KINDS)
|
|
3302
|
+
) {
|
|
3303
|
+
return allowed(
|
|
3304
|
+
KubectlCommandTier.SafeWrite,
|
|
3305
|
+
"deleting one named pod is a bounded change: its controller recreates it",
|
|
3306
|
+
verbLabel,
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
return allowed(
|
|
3311
|
+
KubectlCommandTier.RiskyWrite,
|
|
3312
|
+
hasForce
|
|
3313
|
+
? "force-deleting skips graceful termination"
|
|
3314
|
+
: hasSelector
|
|
3315
|
+
? "deleting by selector can remove many objects at once"
|
|
3316
|
+
: targets.namedCount > 1
|
|
3317
|
+
? "deleting several objects at once"
|
|
3318
|
+
: targets.kinds.has("job")
|
|
3319
|
+
? "a deleted Job is gone for good: no controller recreates it"
|
|
3320
|
+
: `deleting ${writtenKindList} objects changes what is deployed`,
|
|
3321
|
+
verbLabel,
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
if (verb === "scale") {
|
|
3326
|
+
if (hasAll || hasSelector) {
|
|
3327
|
+
return allowed(
|
|
3328
|
+
KubectlCommandTier.RiskyWrite,
|
|
3329
|
+
"kubectl scale with a selector or --all touches many objects at once",
|
|
3330
|
+
verb,
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
const replicas: Array<string> = parsed.flags.get("replicas") || [];
|
|
3335
|
+
|
|
3336
|
+
if (replicas.length === 0) {
|
|
3337
|
+
return deny("kubectl scale needs --replicas", verb);
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
if (!isOneNamedObject(targets)) {
|
|
3341
|
+
return allowed(
|
|
3342
|
+
KubectlCommandTier.RiskyWrite,
|
|
3343
|
+
"kubectl scale names several workloads (or none) at once",
|
|
3344
|
+
verb,
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
if (!everyBuiltinKindIn(targets, SCALE_SAFE_KINDS)) {
|
|
3349
|
+
return allowed(
|
|
3350
|
+
KubectlCommandTier.RiskyWrite,
|
|
3351
|
+
`kubectl scale of ${writtenKindList} is not a safe change (only a Deployment, StatefulSet or ReplicaSet is)`,
|
|
3352
|
+
verb,
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
for (const value of replicas) {
|
|
3357
|
+
const count: string = value.trim();
|
|
3358
|
+
if (!REPLICA_COUNT_PATTERN.test(count)) {
|
|
3359
|
+
return allowed(
|
|
3360
|
+
KubectlCommandTier.RiskyWrite,
|
|
3361
|
+
`--replicas=${value} is not a replica count kubectl scale can apply safely`,
|
|
3362
|
+
verb,
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
3365
|
+
if (parseInt(count, 10) === 0) {
|
|
3366
|
+
return allowed(
|
|
3367
|
+
KubectlCommandTier.RiskyWrite,
|
|
3368
|
+
"scaling to zero replicas stops the workload: an outage, not a reversible nudge",
|
|
3369
|
+
verb,
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
return allowed(
|
|
3375
|
+
KubectlCommandTier.SafeWrite,
|
|
3376
|
+
"kubectl scale of one named workload to a non-zero count is a reversible change",
|
|
3377
|
+
verb,
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
if (verb === "cordon" || verb === "uncordon") {
|
|
3382
|
+
if (hasAll || hasSelector) {
|
|
3383
|
+
return allowed(
|
|
3384
|
+
KubectlCommandTier.RiskyWrite,
|
|
3385
|
+
`kubectl ${verb} with a selector or --all touches many nodes at once`,
|
|
3386
|
+
verb,
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
const nodes: Array<string> = parsed.positionals.slice(1);
|
|
3391
|
+
const node: string = nodes[0] || "";
|
|
3392
|
+
const namesOneNode: boolean =
|
|
3393
|
+
nodes.length === 1 &&
|
|
3394
|
+
(!node.includes("/") ||
|
|
3395
|
+
builtinKind(node.slice(0, node.indexOf("/"))) === "node");
|
|
3396
|
+
|
|
3397
|
+
if (!namesOneNode) {
|
|
3398
|
+
return allowed(
|
|
3399
|
+
KubectlCommandTier.RiskyWrite,
|
|
3400
|
+
`kubectl ${verb} of anything but one named node touches several nodes (or none) at once`,
|
|
3401
|
+
verb,
|
|
3402
|
+
);
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
return allowed(
|
|
3406
|
+
KubectlCommandTier.SafeWrite,
|
|
3407
|
+
`kubectl ${verb} of one node is a reversible change`,
|
|
3408
|
+
verb,
|
|
3409
|
+
);
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
if (verb === "label" || verb === "annotate") {
|
|
3413
|
+
const keys: Array<string> = labelKeys(verb, parsed.positionals);
|
|
3414
|
+
|
|
3415
|
+
if (hasAll || hasSelector) {
|
|
3416
|
+
return allowed(
|
|
3417
|
+
KubectlCommandTier.RiskyWrite,
|
|
3418
|
+
`kubectl ${verb} with a selector or --all touches many objects at once`,
|
|
3419
|
+
verb,
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
if (!isOneNamedObject(targets)) {
|
|
3424
|
+
return allowed(
|
|
3425
|
+
KubectlCommandTier.RiskyWrite,
|
|
3426
|
+
`kubectl ${verb} names several objects (or none) at once`,
|
|
3427
|
+
verb,
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
if (!everyBuiltinKindIn(targets, LABEL_SAFE_KINDS)) {
|
|
3432
|
+
return allowed(
|
|
3433
|
+
KubectlCommandTier.RiskyWrite,
|
|
3434
|
+
`${verb === "label" ? "labels" : "annotations"} on ${writtenKindList} can steer admission, scheduling, identity or traffic (only a pod or workload is a safe target)`,
|
|
3435
|
+
verb,
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
const reservedKey: string | undefined = keys.find(isReservedKey);
|
|
3440
|
+
|
|
3441
|
+
if (reservedKey) {
|
|
3442
|
+
return allowed(
|
|
3443
|
+
KubectlCommandTier.RiskyWrite,
|
|
3444
|
+
`${reservedKey} is a key Kubernetes or a cluster controller acts on, not a plain ${verb === "label" ? "label" : "annotation"}`,
|
|
3445
|
+
verb,
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
return allowed(
|
|
3450
|
+
KubectlCommandTier.SafeWrite,
|
|
3451
|
+
`kubectl ${verb} of one named pod or workload is a reversible change`,
|
|
3452
|
+
verb,
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
if (verb === "patch") {
|
|
3457
|
+
const bodies: Array<string> = [
|
|
3458
|
+
...(parsed.flags.get("p") || []),
|
|
3459
|
+
...(parsed.flags.get("patch") || []),
|
|
3460
|
+
];
|
|
3461
|
+
const patchTypes: Set<PatchType> = readPatchTypes(
|
|
3462
|
+
parsed.flags.get("type") || [],
|
|
3463
|
+
);
|
|
3464
|
+
|
|
3465
|
+
for (const body of bodies) {
|
|
3466
|
+
const problem: string | null = findPatchBodyProblem(body, patchTypes);
|
|
3467
|
+
if (problem) {
|
|
3468
|
+
return deny(problem, "patch");
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
if (RISKY_WRITE_VERBS.has(verb)) {
|
|
3474
|
+
const verbLabel: string =
|
|
3475
|
+
verb === "set" || verb === "create" ? `${verb} ${subcommand}` : verb;
|
|
3476
|
+
return allowed(
|
|
3477
|
+
KubectlCommandTier.RiskyWrite,
|
|
3478
|
+
`kubectl ${verbLabel} can change what is deployed or affect many pods, so a human must approve it`,
|
|
3479
|
+
verbLabel,
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
return deny(
|
|
3484
|
+
`kubectl ${verb} is not a command OneUptime AI may run (for a word it does not know, kubectl would look for a kubectl-${verb} plugin on the Runner and run it)`,
|
|
3485
|
+
verb,
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
/*
|
|
3490
|
+
* The remediation verdict for one command under a cluster's settings.
|
|
3491
|
+
* Read and SafeWrite auto-approve; RiskyWrite auto-approves only when the
|
|
3492
|
+
* cluster bypasses approvals or the operator allowlisted its exact shape
|
|
3493
|
+
* (matchesAllowlist: token by token, see the allowlist section in the
|
|
3494
|
+
* header); Denied stays Denied; and a write in a protected namespace, a
|
|
3495
|
+
* node drain, a taint and a patch of a Node always need a human. Suggest
|
|
3496
|
+
* mode never consults this — everything there is RequiresApproval by
|
|
3497
|
+
* construction.
|
|
3498
|
+
*/
|
|
3499
|
+
public static evaluateForAutoExecution(data: {
|
|
3500
|
+
command: string;
|
|
3501
|
+
allowlistPatterns: Array<string>;
|
|
3502
|
+
/*
|
|
3503
|
+
* The cluster's operator chose to never be asked: RiskyWrite auto-approves
|
|
3504
|
+
* too. Denied stays Denied — that tier is what "even with approval"
|
|
3505
|
+
* means, and bypassing approval cannot grant more than approval would.
|
|
3506
|
+
* Nor does it reach a protected namespace (kube-system and friends), a
|
|
3507
|
+
* drain, a taint or a patch of a Node.
|
|
3508
|
+
*/
|
|
3509
|
+
bypassApproval?: boolean | undefined;
|
|
3510
|
+
}): KubectlAutoExecutionVerdict {
|
|
3511
|
+
const result: KubectlPolicyResult = KubectlPolicy.evaluateCommand(
|
|
3512
|
+
data.command,
|
|
3513
|
+
);
|
|
3514
|
+
|
|
3515
|
+
if (result.tier === KubectlCommandTier.Denied) {
|
|
3516
|
+
return {
|
|
3517
|
+
verdict: AiRemediationCommandPolicyVerdict.Denied,
|
|
3518
|
+
tier: result.tier,
|
|
3519
|
+
reason: `Denied by the kubectl command policy: ${result.reason}. This command cannot run even with human approval.`,
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
if (result.tier === KubectlCommandTier.Read) {
|
|
3524
|
+
return {
|
|
3525
|
+
verdict: AiRemediationCommandPolicyVerdict.AutoApproved,
|
|
3526
|
+
tier: result.tier,
|
|
3527
|
+
reason: result.reason,
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
/*
|
|
3532
|
+
* Before SafeWrite, bypass and the allowlist: nothing an operator set on
|
|
3533
|
+
* the cluster lets OneUptime AI change kube-system on its own.
|
|
3534
|
+
*/
|
|
3535
|
+
if (result.protectedNamespace) {
|
|
3536
|
+
return {
|
|
3537
|
+
verdict: AiRemediationCommandPolicyVerdict.RequiresApproval,
|
|
3538
|
+
tier: result.tier,
|
|
3539
|
+
reason: `Requires human approval: ${result.reason}. Neither bypassing approvals nor the cluster's allowlist applies in ${result.protectedNamespace}.`,
|
|
3540
|
+
requiresHuman: true,
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// Nor does anything let it drain, taint or patch a node on its own.
|
|
3545
|
+
if (result.requiresHuman) {
|
|
3546
|
+
return {
|
|
3547
|
+
verdict: AiRemediationCommandPolicyVerdict.RequiresApproval,
|
|
3548
|
+
tier: result.tier,
|
|
3549
|
+
reason: `Requires human approval: ${result.reason}. Neither bypassing approvals nor the cluster's allowlist applies to a node drain or taint, or to a patch of a Node.`,
|
|
3550
|
+
requiresHuman: true,
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
if (result.tier === KubectlCommandTier.SafeWrite) {
|
|
3555
|
+
return {
|
|
3556
|
+
verdict: AiRemediationCommandPolicyVerdict.AutoApproved,
|
|
3557
|
+
tier: result.tier,
|
|
3558
|
+
reason: result.reason,
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
if (data.bypassApproval === true) {
|
|
3563
|
+
return {
|
|
3564
|
+
verdict: AiRemediationCommandPolicyVerdict.AutoApproved,
|
|
3565
|
+
tier: result.tier,
|
|
3566
|
+
reason: `Riskier change (${result.reason}) allowed without approval: the cluster bypasses approvals.`,
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
if (
|
|
3571
|
+
KubectlPolicy.matchesAllowlist({
|
|
3572
|
+
args: result.args,
|
|
3573
|
+
allowlistPatterns: data.allowlistPatterns,
|
|
3574
|
+
})
|
|
3575
|
+
) {
|
|
3576
|
+
return {
|
|
3577
|
+
verdict: AiRemediationCommandPolicyVerdict.AutoApproved,
|
|
3578
|
+
tier: result.tier,
|
|
3579
|
+
reason: "Matched the cluster's kubectl allowlist.",
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
return {
|
|
3584
|
+
verdict: AiRemediationCommandPolicyVerdict.RequiresApproval,
|
|
3585
|
+
tier: result.tier,
|
|
3586
|
+
reason: `Requires human approval: ${result.reason}.`,
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
/*
|
|
3591
|
+
* Whether a cluster allowlist pattern names this argv (without the
|
|
3592
|
+
* leading "kubectl"), token by token — the semantics are spelled out in
|
|
3593
|
+
* the allowlist section of the header. Tier-blind on purpose: callers
|
|
3594
|
+
* (evaluateForAutoExecution) decide which tiers the allowlist may promote.
|
|
3595
|
+
*/
|
|
3596
|
+
public static matchesAllowlist(data: {
|
|
3597
|
+
args: Array<string>;
|
|
3598
|
+
allowlistPatterns: Array<string>;
|
|
3599
|
+
}): boolean {
|
|
3600
|
+
const args: Array<string> = (data.args || []).filter((arg: unknown) => {
|
|
3601
|
+
return typeof arg === "string";
|
|
3602
|
+
});
|
|
3603
|
+
|
|
3604
|
+
if (args.length === 0) {
|
|
3605
|
+
return false;
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
const patterns: Array<string> = (data.allowlistPatterns || [])
|
|
3609
|
+
.filter((pattern: unknown) => {
|
|
3610
|
+
return typeof pattern === "string" && pattern.trim().length > 0;
|
|
3611
|
+
})
|
|
3612
|
+
.slice(0, MAX_ALLOWLIST_PATTERNS);
|
|
3613
|
+
|
|
3614
|
+
for (const pattern of patterns) {
|
|
3615
|
+
/*
|
|
3616
|
+
* An entry describeAllowlistPatternProblem refuses is skipped, so what
|
|
3617
|
+
* the matcher reads and what the server and the AI page accept are
|
|
3618
|
+
* one set — including an entry stored before a rule was added.
|
|
3619
|
+
*/
|
|
3620
|
+
const patternArgs: Array<string> | undefined =
|
|
3621
|
+
KubectlPolicy.readAllowlistEntry(pattern).args;
|
|
3622
|
+
|
|
3623
|
+
if (!patternArgs || patternArgs.length !== args.length) {
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
const matches: boolean = patternArgs.every(
|
|
3628
|
+
(patternToken: string, index: number) => {
|
|
3629
|
+
return allowlistTokenMatches(patternToken, args[index]!);
|
|
3630
|
+
},
|
|
3631
|
+
);
|
|
3632
|
+
|
|
3633
|
+
if (matches) {
|
|
3634
|
+
return true;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
return false;
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
/*
|
|
3642
|
+
* Why an allowlist entry cannot work, in words for the person typing it —
|
|
3643
|
+
* or null when it is valid. This is the ONE definition of a valid entry
|
|
3644
|
+
* (see the allowlist section of the header): the server validates saves
|
|
3645
|
+
* with it, the AI page validates its form with it, and matchesAllowlist
|
|
3646
|
+
* skips every entry it refuses, so no entry is stored that the matcher
|
|
3647
|
+
* would silently ignore. (The entry count is checked by the callers
|
|
3648
|
+
* against KUBECTL_ALLOWLIST_MAX_PATTERNS.)
|
|
3649
|
+
*/
|
|
3650
|
+
public static describeAllowlistPatternProblem(
|
|
3651
|
+
pattern: unknown,
|
|
3652
|
+
): string | null {
|
|
3653
|
+
return KubectlPolicy.readAllowlistEntry(pattern).problem || null;
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
/*
|
|
3657
|
+
* Does this allowlist entry pre-approve riskier changes beyond the objects
|
|
3658
|
+
* it names? True when a `*` stands for the namespace, for an object the
|
|
3659
|
+
* command acts on, or for a selector or --from source — see the allowlist
|
|
3660
|
+
* section of the header. The AI page asks for an explicit confirmation
|
|
3661
|
+
* before saving a broad entry.
|
|
3662
|
+
*
|
|
3663
|
+
* The entry is read with the policy's own tokenizer and flag parser, the
|
|
3664
|
+
* way matchesAllowlist reads it (a leading "kubectl" is dropped once, in
|
|
3665
|
+
* any case), so every spelling of -n (`-n *`, `-n=*`, `--namespace=*`, a
|
|
3666
|
+
* cluster such as `-An *`) is seen, and a value flag's `*` (`-p *`,
|
|
3667
|
+
* `--replicas *`) is never mistaken for an object. An invalid entry is
|
|
3668
|
+
* never broad: it is refused before anyone could be asked to confirm it.
|
|
3669
|
+
*/
|
|
3670
|
+
public static isBroadAllowlistPattern(pattern: unknown): boolean {
|
|
3671
|
+
const entry: AllowlistEntry = KubectlPolicy.readAllowlistEntry(pattern);
|
|
3672
|
+
|
|
3673
|
+
if (!entry.parsed) {
|
|
3674
|
+
return false;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
const parsed: ParsedArgs = entry.parsed;
|
|
3678
|
+
const verb: string = parsed.positionals[0] || "";
|
|
3679
|
+
const subcommand: string = parsed.positionals[1] || "";
|
|
3680
|
+
|
|
3681
|
+
/*
|
|
3682
|
+
* Reads, drains, taints and patches of a Node are never promoted: the
|
|
3683
|
+
* entry pre-approves nothing.
|
|
3684
|
+
*/
|
|
3685
|
+
if (!allowlistMayPromote(parsed)) {
|
|
3686
|
+
return false;
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
for (const flag of WIDENING_FLAGS) {
|
|
3690
|
+
for (const value of parsed.flags.get(flag) || []) {
|
|
3691
|
+
if (value.includes("*")) {
|
|
3692
|
+
return true;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
// A wildcard in create's positionals names the NEW object, not an existing one.
|
|
3698
|
+
if (verb === "create") {
|
|
3699
|
+
return false;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
let operands: Array<string> = parsed.positionals.slice(
|
|
3703
|
+
SUBCOMMAND_VERBS.has(verb) ? 2 : 1,
|
|
3704
|
+
);
|
|
3705
|
+
|
|
3706
|
+
// kubectl set selector reads its LAST word as the selector expression.
|
|
3707
|
+
if (verb === "set" && subcommand === "selector") {
|
|
3708
|
+
operands = operands.slice(0, -1);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
if (!readsUpdatePairs(verb, subcommand, parsed.flags)) {
|
|
3712
|
+
return operands.some(hasWildcard);
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
/*
|
|
3716
|
+
* label, annotate, set image and set env read their operands the way
|
|
3717
|
+
* kubectl's GetResourcesAndPairs does: objects first, then the updates
|
|
3718
|
+
* (KEY=VALUE or KEY-), and an object after an update is an error. So a
|
|
3719
|
+
* wildcard can stand for an object only before the first update, and
|
|
3720
|
+
* only when another word follows it: a wildcard that is the LAST word
|
|
3721
|
+
* is read either as the update or as one more object with no update at
|
|
3722
|
+
* all, which kubectl refuses ("at least one ... update is required").
|
|
3723
|
+
*/
|
|
3724
|
+
for (let i: number = 0; i < operands.length; i++) {
|
|
3725
|
+
const operand: string = operands[i]!;
|
|
3726
|
+
|
|
3727
|
+
if (isUpdatePairToken(operand)) {
|
|
3728
|
+
return false;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
if (hasWildcard(operand) && i < operands.length - 1) {
|
|
3732
|
+
return true;
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
return false;
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
/*
|
|
3740
|
+
* An allowlist entry read the way matchesAllowlist reads it: its argv
|
|
3741
|
+
* (the optional leading "kubectl" dropped by tokenize) and its parse when
|
|
3742
|
+
* it is valid, or the problem that makes it invalid.
|
|
3743
|
+
*/
|
|
3744
|
+
private static readAllowlistEntry(pattern: unknown): AllowlistEntry {
|
|
3745
|
+
if (typeof pattern !== "string" || pattern.trim().length === 0) {
|
|
3746
|
+
return { problem: "An allowlist entry cannot be blank." };
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
if (pattern.length > MAX_ALLOWLIST_PATTERN_LENGTH) {
|
|
3750
|
+
return {
|
|
3751
|
+
problem: `An allowlist entry can be at most ${MAX_ALLOWLIST_PATTERN_LENGTH} characters long.`,
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
const shown: string = pattern.trim();
|
|
3756
|
+
const namesNoCommand: string = `"${shown}" names no kubectl command: write out the command it pre-approves, such as "kubectl set image deployment/web * -n web".`;
|
|
3757
|
+
const tokenized: KubectlTokenizeResult = KubectlPolicy.tokenize(pattern);
|
|
3758
|
+
|
|
3759
|
+
if (!tokenized.args) {
|
|
3760
|
+
return {
|
|
3761
|
+
problem:
|
|
3762
|
+
tokenized.errorMessage === NO_VERB_MESSAGE
|
|
3763
|
+
? namesNoCommand
|
|
3764
|
+
: `"${shown}" cannot be read as one kubectl command: ${
|
|
3765
|
+
tokenized.errorMessage || "it could not be split into words."
|
|
3766
|
+
}`,
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
/*
|
|
3771
|
+
* One word after the optional "kubectl" can only match a one-word
|
|
3772
|
+
* command, and none is ever pre-approved: a read runs anyway, and every
|
|
3773
|
+
* write verb needs an object (and a change) to act on, so kubectl
|
|
3774
|
+
* refuses it bare. Quotes around a full command (a pasted JSON list, a
|
|
3775
|
+
* quoted line) make it one word too.
|
|
3776
|
+
*/
|
|
3777
|
+
const quotedWords: string = `"${shown}" has quotes around several words where the kubectl verb goes, so they are read as one word (as in a pasted JSON list, or a full command in quotes). Write each entry on its own line without quotes or brackets, such as kubectl set image deployment/web * -n web.`;
|
|
3778
|
+
|
|
3779
|
+
if (tokenized.args.length === 1) {
|
|
3780
|
+
return {
|
|
3781
|
+
problem: WHITESPACE_REGEX.test(tokenized.args[0]!)
|
|
3782
|
+
? quotedWords
|
|
3783
|
+
: `"${shown}" is only one word after the optional "kubectl", so it could only match a one-word command, and a one-word command is never pre-approved (a read runs anyway; kubectl refuses a write verb with nothing to act on). Write out the full command it pre-approves, such as "kubectl set image deployment/web * -n web".`,
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
const parsed: ParsedArgs = KubectlPolicy.parseArgs(tokenized.args);
|
|
3788
|
+
const verb: string | undefined = parsed.positionals[0];
|
|
3789
|
+
|
|
3790
|
+
if (verb === undefined) {
|
|
3791
|
+
return { problem: namesNoCommand };
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
if (WHITESPACE_REGEX.test(verb)) {
|
|
3795
|
+
return { problem: quotedWords };
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
if (hasWildcard(verb)) {
|
|
3799
|
+
return {
|
|
3800
|
+
problem: `"${shown}" has a * where the kubectl verb goes. A * stands for exactly one word, and the first word is the verb (the leading "kubectl" is optional and never needs a wildcard), so write the verb out, as in "kubectl set image deployment/web * -n web".`,
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
if (verb !== verb.toLowerCase()) {
|
|
3805
|
+
return {
|
|
3806
|
+
problem: `"${shown}" can never match a command that runs: kubectl matches commands case-sensitively, so "${verb}" is not kubectl ${verb.toLowerCase()} (write the verb in lowercase).`,
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
if (!isVerbOneUptimeAiMayRun(verb)) {
|
|
3811
|
+
return {
|
|
3812
|
+
problem: `"${shown}" can never match a command that runs: kubectl ${verb} is not a command OneUptime AI may run.`,
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
if (SUBCOMMAND_VERBS.has(verb)) {
|
|
3817
|
+
const subcommand: string | undefined = parsed.positionals[1];
|
|
3818
|
+
|
|
3819
|
+
if (subcommand === undefined) {
|
|
3820
|
+
if (SUBCOMMAND_REQUIRED_VERBS.has(verb)) {
|
|
3821
|
+
return {
|
|
3822
|
+
problem: `"${shown}" names no kubectl ${verb} subcommand: write it out${subcommandExample(verb)}.`,
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3825
|
+
} else if (hasWildcard(subcommand)) {
|
|
3826
|
+
return {
|
|
3827
|
+
problem: `"${shown}" has a * where the kubectl ${verb} subcommand goes: a * stands for exactly one word, and the subcommand decides what kind of change runs, so write it out${subcommandExample(verb)}.`,
|
|
3828
|
+
};
|
|
3829
|
+
} else if (subcommand !== subcommand.toLowerCase()) {
|
|
3830
|
+
return {
|
|
3831
|
+
problem: `"${shown}" can never match a command that runs: kubectl matches commands case-sensitively, so "${subcommand}" is not the kubectl ${verb} ${subcommand.toLowerCase()} command (write the subcommand in lowercase).`,
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
if (parsed.violation) {
|
|
3837
|
+
return {
|
|
3838
|
+
problem: `"${shown}" can never match a command that runs: ${parsed.violation}.`,
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
/*
|
|
3843
|
+
* Refused by evaluateArgs rather than by the parser (see
|
|
3844
|
+
* OBJECT_OVERRIDE_FLAGS), so every command such an entry could match is
|
|
3845
|
+
* Denied all the same.
|
|
3846
|
+
*/
|
|
3847
|
+
const overrideFlag: string | undefined = OBJECT_OVERRIDE_FLAGS.find(
|
|
3848
|
+
(flag: string) => {
|
|
3849
|
+
return parsed.flags.has(flag);
|
|
3850
|
+
},
|
|
3851
|
+
);
|
|
3852
|
+
|
|
3853
|
+
if (overrideFlag !== undefined) {
|
|
3854
|
+
return {
|
|
3855
|
+
problem: `"${shown}" can never match a command that runs: the --${overrideFlag} flag is never allowed for OneUptime AI, because --overrides lets kubectl expose create an object of any kind, in any namespace, instead of the Service the command names.`,
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
return { args: tokenized.args, parsed };
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
public static isReadOnly(command: string): boolean {
|
|
3863
|
+
return (
|
|
3864
|
+
KubectlPolicy.evaluateCommand(command).tier === KubectlCommandTier.Read
|
|
3865
|
+
);
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
/*
|
|
3869
|
+
* Render an argv back to the one-line form humans read on approval cards
|
|
3870
|
+
* and in the activity feed. Tokens that would not survive a shell split
|
|
3871
|
+
* are single-quoted, so the rendering round-trips through tokenize().
|
|
3872
|
+
*/
|
|
3873
|
+
public static renderDisplayCommand(args: Array<string>): string {
|
|
3874
|
+
const rendered: Array<string> = ["kubectl"];
|
|
3875
|
+
|
|
3876
|
+
for (const arg of args) {
|
|
3877
|
+
if (arg === "") {
|
|
3878
|
+
rendered.push("''");
|
|
3879
|
+
} else if (SHELL_SAFE_TOKEN.test(arg)) {
|
|
3880
|
+
rendered.push(arg);
|
|
3881
|
+
} else {
|
|
3882
|
+
rendered.push(`'${arg.replace(/'/g, `'\\''`)}'`);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
return rendered.join(" ");
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
/*
|
|
3890
|
+
* What a flag name means here: its arity, and whether it may be used at
|
|
3891
|
+
* all. `verb` is the first positional seen so far (undefined before it),
|
|
3892
|
+
* for the few flags whose meaning depends on it.
|
|
3893
|
+
*/
|
|
3894
|
+
private static lookupFlag(name: string, verb: string | undefined): FlagRule {
|
|
3895
|
+
const override: FlagArity | undefined =
|
|
3896
|
+
verb !== undefined
|
|
3897
|
+
? VERB_FLAG_OVERRIDE_ARITY.get(verb)?.get(name)
|
|
3898
|
+
: undefined;
|
|
3899
|
+
|
|
3900
|
+
if (override) {
|
|
3901
|
+
return { arity: override };
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
const denyReason: string | undefined = DENIED_FLAG_REASONS.get(name);
|
|
3905
|
+
|
|
3906
|
+
if (denyReason) {
|
|
3907
|
+
return {
|
|
3908
|
+
arity: FlagArity.Boolean,
|
|
3909
|
+
problem: `is not allowed: ${denyReason}`,
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
const arity: FlagArity | undefined = KNOWN_FLAG_ARITY.get(name);
|
|
3914
|
+
|
|
3915
|
+
if (arity) {
|
|
3916
|
+
return { arity };
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
return {
|
|
3920
|
+
arity: FlagArity.Boolean,
|
|
3921
|
+
problem:
|
|
3922
|
+
"is not a kubectl flag OneUptime AI may use (an unknown flag could hide the verb or the resource behind its value, so it is refused rather than guessed)",
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// Streaming flags may only ever be switched off explicitly.
|
|
3927
|
+
private static findStreamingFlag(
|
|
3928
|
+
flags: Map<string, Array<string>>,
|
|
3929
|
+
): string | null {
|
|
3930
|
+
for (const name of STREAMING_FLAGS) {
|
|
3931
|
+
const values: Array<string> | undefined = flags.get(name);
|
|
3932
|
+
|
|
3933
|
+
if (!values) {
|
|
3934
|
+
continue;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
for (const value of values) {
|
|
3938
|
+
if (value !== "false") {
|
|
3939
|
+
const shown: string = name.length === 1 ? `-${name}` : `--${name}`;
|
|
3940
|
+
return `the ${shown} flag streams until kubectl is killed; OneUptime AI reads a bounded snapshot (${shown}=false is fine)`;
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
return null;
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
// Every -o/--output value must be an inline format kubectl prints itself.
|
|
3949
|
+
private static findOutputFormatProblem(
|
|
3950
|
+
flags: Map<string, Array<string>>,
|
|
3951
|
+
): string | null {
|
|
3952
|
+
const values: Array<string> = [
|
|
3953
|
+
...(flags.get("o") || []),
|
|
3954
|
+
...(flags.get("output") || []),
|
|
3955
|
+
];
|
|
3956
|
+
|
|
3957
|
+
for (const value of values) {
|
|
3958
|
+
const lower: string = value.toLowerCase();
|
|
3959
|
+
const eq: number = lower.indexOf("=");
|
|
3960
|
+
const format: string = eq >= 0 ? lower.slice(0, eq) : lower;
|
|
3961
|
+
|
|
3962
|
+
if (FILE_OUTPUT_FORMATS.has(format)) {
|
|
3963
|
+
return `the output format "${value}" makes kubectl read a file on the Runner and echo it; write the template inline (-o jsonpath=..., -o go-template=..., -o custom-columns=...)`;
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
/*
|
|
3967
|
+
* A template format without its template would take it from
|
|
3968
|
+
* --template, which is Denied, so the bare form can never run.
|
|
3969
|
+
*/
|
|
3970
|
+
if (OUTPUT_FORMATS_WITH_ARGUMENT.has(format)) {
|
|
3971
|
+
if (eq < 0) {
|
|
3972
|
+
return `the output format "${value}" needs its template inline: -o ${format}=... (--template is not allowed)`;
|
|
3973
|
+
}
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
if (eq >= 0 || !OUTPUT_FORMATS.has(format)) {
|
|
3978
|
+
return `the output format "${value}" is not one OneUptime AI may use (json, yaml, name, wide, jsonpath=..., go-template=..., custom-columns=...)`;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
return null;
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
/*
|
|
3986
|
+
* Split an argv into positionals and flags exactly as pflag will (see the
|
|
3987
|
+
* Flags section above). Never throws and never stops early: it records the
|
|
3988
|
+
* first problem and keeps going, so the caller can still name the verb.
|
|
3989
|
+
*/
|
|
3990
|
+
private static parseArgs(args: Array<string>): ParsedArgs {
|
|
3991
|
+
const positionals: Array<string> = [];
|
|
3992
|
+
const flags: Map<string, Array<string>> = new Map<string, Array<string>>();
|
|
3993
|
+
const problems: Array<string> = [];
|
|
3994
|
+
let verb: string | undefined = undefined;
|
|
3995
|
+
let subcommand: string | undefined = undefined;
|
|
3996
|
+
let afterDoubleDash: boolean = false;
|
|
3997
|
+
|
|
3998
|
+
const record: (name: string, value: string) => void = (
|
|
3999
|
+
name: string,
|
|
4000
|
+
value: string,
|
|
4001
|
+
): void => {
|
|
4002
|
+
const existing: Array<string> = flags.get(name) || [];
|
|
4003
|
+
existing.push(value);
|
|
4004
|
+
flags.set(name, existing);
|
|
4005
|
+
};
|
|
4006
|
+
|
|
4007
|
+
/*
|
|
4008
|
+
* True once the positionals name the command kubectl will run: the verb,
|
|
4009
|
+
* plus its subcommand for SUBCOMMAND_VERBS. Until then only GLOBAL_FLAGS
|
|
4010
|
+
* may appear (see the Flags section: kubectl picks the command first and
|
|
4011
|
+
* would take the word after any other flag as that flag's value).
|
|
4012
|
+
*/
|
|
4013
|
+
const commandFound: () => boolean = (): boolean => {
|
|
4014
|
+
return (
|
|
4015
|
+
verb !== undefined &&
|
|
4016
|
+
(!SUBCOMMAND_VERBS.has(verb) || subcommand !== undefined)
|
|
4017
|
+
);
|
|
4018
|
+
};
|
|
4019
|
+
|
|
4020
|
+
const orderingProblem: (name: string, shown: string) => string | null = (
|
|
4021
|
+
name: string,
|
|
4022
|
+
shown: string,
|
|
4023
|
+
): string | null => {
|
|
4024
|
+
if (commandFound() || GLOBAL_FLAGS.has(name)) {
|
|
4025
|
+
return null;
|
|
4026
|
+
}
|
|
4027
|
+
const after: string =
|
|
4028
|
+
verb === undefined ? "the verb" : `the kubectl ${verb} subcommand`;
|
|
4029
|
+
return `the ${shown} flag must come after ${after}: kubectl picks the command before it parses flags and would take the word after this flag as its value`;
|
|
4030
|
+
};
|
|
4031
|
+
|
|
4032
|
+
for (let i: number = 0; i < args.length; i++) {
|
|
4033
|
+
const token: string = args[i]!;
|
|
4034
|
+
|
|
4035
|
+
if (afterDoubleDash || !token.startsWith("-") || token === "-") {
|
|
4036
|
+
positionals.push(token);
|
|
4037
|
+
if (verb === undefined) {
|
|
4038
|
+
verb = token.toLowerCase();
|
|
4039
|
+
} else if (subcommand === undefined) {
|
|
4040
|
+
subcommand = token.toLowerCase();
|
|
4041
|
+
}
|
|
4042
|
+
continue;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
if (token === "--") {
|
|
4046
|
+
if (!commandFound()) {
|
|
4047
|
+
problems.push(`"--" must come after the verb`);
|
|
4048
|
+
}
|
|
4049
|
+
afterDoubleDash = true;
|
|
4050
|
+
continue;
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
if (token.startsWith("--")) {
|
|
4054
|
+
const eq: number = token.indexOf("=");
|
|
4055
|
+
const rawName: string = eq >= 0 ? token.slice(2, eq) : token.slice(2);
|
|
4056
|
+
const name: string = normalizeKubectlFlagName(rawName);
|
|
4057
|
+
|
|
4058
|
+
if (!name || name.startsWith("-")) {
|
|
4059
|
+
problems.push(`"${token}" is not valid flag syntax`);
|
|
4060
|
+
continue;
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
const rule: FlagRule = KubectlPolicy.lookupFlag(name, verb);
|
|
4064
|
+
const ordering: string | null = orderingProblem(name, `--${rawName}`);
|
|
4065
|
+
|
|
4066
|
+
if (rule.problem) {
|
|
4067
|
+
problems.push(`the --${rawName} flag ${rule.problem}`);
|
|
4068
|
+
} else if (ordering) {
|
|
4069
|
+
problems.push(ordering);
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
if (eq >= 0) {
|
|
4073
|
+
record(name, token.slice(eq + 1));
|
|
4074
|
+
} else if (rule.arity === FlagArity.Value && i + 1 < args.length) {
|
|
4075
|
+
record(name, args[i + 1]!);
|
|
4076
|
+
i++;
|
|
4077
|
+
} else {
|
|
4078
|
+
record(name, "");
|
|
4079
|
+
}
|
|
4080
|
+
continue;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
// "-abc": a cluster of short flags, read letter by letter like pflag.
|
|
4084
|
+
let rest: string = token.slice(1);
|
|
4085
|
+
|
|
4086
|
+
while (rest.length > 0) {
|
|
4087
|
+
const letter: string = rest.charAt(0);
|
|
4088
|
+
const rule: FlagRule = KubectlPolicy.lookupFlag(letter, verb);
|
|
4089
|
+
const where: string =
|
|
4090
|
+
token === `-${letter}` || token.startsWith(`-${letter}=`)
|
|
4091
|
+
? ""
|
|
4092
|
+
: ` (in "${token}")`;
|
|
4093
|
+
const ordering: string | null = orderingProblem(
|
|
4094
|
+
letter,
|
|
4095
|
+
`-${letter}${where}`,
|
|
4096
|
+
);
|
|
4097
|
+
|
|
4098
|
+
if (rule.problem) {
|
|
4099
|
+
problems.push(`the -${letter} flag${where} ${rule.problem}`);
|
|
4100
|
+
} else if (ordering) {
|
|
4101
|
+
problems.push(ordering);
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
// "-n=web"
|
|
4105
|
+
if (rest.charAt(1) === "=") {
|
|
4106
|
+
record(letter, rest.slice(2));
|
|
4107
|
+
break;
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
// A boolean (or optional-value) letter: the next letter is another flag.
|
|
4111
|
+
if (rule.arity !== FlagArity.Value) {
|
|
4112
|
+
record(letter, "");
|
|
4113
|
+
rest = rest.slice(1);
|
|
4114
|
+
continue;
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
// "-nweb"
|
|
4118
|
+
if (rest.length > 1) {
|
|
4119
|
+
record(letter, rest.slice(1));
|
|
4120
|
+
break;
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// "-n web"
|
|
4124
|
+
if (i + 1 < args.length) {
|
|
4125
|
+
record(letter, args[i + 1]!);
|
|
4126
|
+
i++;
|
|
4127
|
+
} else {
|
|
4128
|
+
record(letter, "");
|
|
4129
|
+
}
|
|
4130
|
+
break;
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
return { positionals, flags, violation: problems[0] };
|
|
4135
|
+
}
|
|
4136
|
+
}
|