@oneuptime/common 14.0.1 → 14.0.3

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.
Files changed (1241) hide show
  1. package/Models/DatabaseModels/AlertEpisodeMember.ts +8 -1
  2. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  3. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +63 -0
  4. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  5. package/Models/DatabaseModels/IncidentEpisodeMember.ts +8 -1
  6. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +9 -2
  7. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  8. package/Models/DatabaseModels/IncidentPublicNote.ts +9 -2
  9. package/Models/DatabaseModels/Index.ts +2 -0
  10. package/Models/DatabaseModels/KubernetesCluster.ts +429 -0
  11. package/Models/DatabaseModels/Monitor.ts +48 -0
  12. package/Models/DatabaseModels/Project.ts +60 -0
  13. package/Models/DatabaseModels/RumApplication.ts +52 -1
  14. package/Models/DatabaseModels/Runner.ts +5 -4
  15. package/Models/DatabaseModels/RunnerJob.ts +66 -0
  16. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +10 -2
  17. package/Models/DatabaseModels/WorkflowVariable.ts +479 -2
  18. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +79 -25
  19. package/Server/API/AIInvestigationAPI.ts +341 -0
  20. package/Server/API/AutoRemediationAPI.ts +184 -2
  21. package/Server/API/CommonAPI.ts +70 -0
  22. package/Server/API/KubernetesClusterAiAccessAPI.ts +793 -0
  23. package/Server/API/MicrosoftTeamsAPI.ts +770 -315
  24. package/Server/API/MonitorAPI.ts +140 -0
  25. package/Server/API/OnCallScheduleTimelineAPI.ts +5 -21
  26. package/Server/API/SlackAPI.ts +447 -99
  27. package/Server/API/StatusPageAPI.ts +146 -26
  28. package/Server/API/TelemetryAPI.ts +216 -5
  29. package/Server/API/UserTwoFactorBackupCodeAPI.ts +1 -0
  30. package/Server/API/WorkflowVariableAPI.ts +156 -0
  31. package/Server/Infrastructure/GlobalCache.ts +35 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.ts +97 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.ts +129 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.ts +25 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.ts +20 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.ts +38 -0
  37. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  38. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  39. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
  40. package/Server/Middleware/MasterAdminAuthorization.ts +28 -0
  41. package/Server/Middleware/TelemetryIngest.ts +47 -2
  42. package/Server/Middleware/UserAuthorization.ts +56 -2
  43. package/Server/Services/AIAgentService.ts +1 -0
  44. package/Server/Services/AccessTokenService.ts +121 -48
  45. package/Server/Services/AlertEpisodeMemberService.ts +9 -0
  46. package/Server/Services/AlertEpisodeService.ts +14 -1
  47. package/Server/Services/AlertService.ts +134 -22
  48. package/Server/Services/AutoRemediationRuleEngineService.ts +968 -39
  49. package/Server/Services/AutoRemediationRuleService.ts +118 -0
  50. package/Server/Services/BillingService.ts +1 -0
  51. package/Server/Services/InboundEmail/Providers/SendGridInboundProvider.ts +16 -22
  52. package/Server/Services/IncidentAlertService.ts +1811 -0
  53. package/Server/Services/IncidentEpisodeMemberService.ts +9 -0
  54. package/Server/Services/IncidentEpisodePublicNoteService.ts +66 -0
  55. package/Server/Services/IncidentEpisodeRoleMemberService.ts +27 -0
  56. package/Server/Services/IncidentEpisodeService.ts +14 -1
  57. package/Server/Services/IncidentGroupingEngineService.ts +11 -0
  58. package/Server/Services/IncidentMemberService.ts +21 -0
  59. package/Server/Services/IncidentPublicNoteService.ts +63 -0
  60. package/Server/Services/IncidentService.ts +330 -30
  61. package/Server/Services/IncidentStateTimelineService.ts +39 -0
  62. package/Server/Services/IncidentTemplateService.ts +83 -3
  63. package/Server/Services/Index.ts +2 -0
  64. package/Server/Services/KubernetesClusterAiAccessService.ts +1943 -0
  65. package/Server/Services/KubernetesClusterService.ts +1570 -1
  66. package/Server/Services/LogAggregationService.ts +14 -6
  67. package/Server/Services/MonitorService.ts +651 -52
  68. package/Server/Services/MonitorStatusTimelineService.ts +639 -2
  69. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -0
  70. package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -0
  71. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -0
  72. package/Server/Services/OnCallDutyPolicyScheduleService.ts +3 -0
  73. package/Server/Services/OnCallNotificationAlertingService.ts +36 -12
  74. package/Server/Services/OnCallSetupReminderService.ts +1 -0
  75. package/Server/Services/ProbeService.ts +7 -1
  76. package/Server/Services/ProjectService.ts +11 -1
  77. package/Server/Services/RunbookCredentialService.ts +26 -1
  78. package/Server/Services/RunbookSecretService.ts +36 -0
  79. package/Server/Services/RunnerJobService.ts +941 -23
  80. package/Server/Services/RunnerService.ts +439 -6
  81. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +66 -0
  82. package/Server/Services/ScheduledMaintenanceService.ts +124 -19
  83. package/Server/Services/ScheduledMaintenanceTemplateService.ts +82 -3
  84. package/Server/Services/ServiceLevelObjectiveService.ts +14 -1
  85. package/Server/Services/StatusPagePrivateUserService.ts +1 -0
  86. package/Server/Services/StatusPageService.ts +301 -74
  87. package/Server/Services/StatusPageSubscriberService.ts +4 -0
  88. package/Server/Services/TeamMemberService.ts +309 -7
  89. package/Server/Services/TraceAggregationService.ts +10 -6
  90. package/Server/Services/UserEmailService.ts +1 -0
  91. package/Server/Services/UserNotificationMethodAdminService.ts +1 -0
  92. package/Server/Services/UserNotificationRuleAdminService.ts +1 -0
  93. package/Server/Services/UserNotificationRuleService.ts +4 -0
  94. package/Server/Services/UserService.ts +122 -1
  95. package/Server/Services/WorkflowVariableService.ts +593 -8
  96. package/Server/Services/WorkspaceNotificationRuleService.ts +588 -8
  97. package/Server/Services/WorkspaceProjectAuthTokenService.ts +170 -1
  98. package/Server/Types/Database/QueryHelper.ts +22 -0
  99. package/Server/Types/Markdown.ts +502 -27
  100. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +182 -0
  101. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +525 -0
  102. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +854 -0
  103. package/Server/Utils/AI/CodeFix/CodeFixAgentAuth.ts +22 -2
  104. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1916 -307
  105. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +1601 -74
  106. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +139 -7
  107. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +58 -1
  108. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +54 -1
  109. package/Server/Utils/AI/Toolbox/AIActionTools.ts +14 -6
  110. package/Server/Utils/AI/Toolbox/ResourceTools.ts +4 -2
  111. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +1341 -150
  112. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +375 -4
  113. package/Server/Utils/Database/AffectedResourceRelations.ts +127 -0
  114. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +240 -1
  115. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +1 -0
  116. package/Server/Utils/Monitor/MonitorAlert.ts +83 -9
  117. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +5 -1
  118. package/Server/Utils/Monitor/MonitorIncident.ts +104 -9
  119. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +124 -7
  120. package/Server/Utils/Monitor/ProbeMonitorsNotification.ts +407 -0
  121. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +1 -0
  122. package/Server/Utils/PushNotificationUtil.ts +45 -0
  123. package/Server/Utils/Rules/OwnerRuleAssignment.ts +40 -3
  124. package/Server/Utils/Runbook/RunbookExecutePermission.ts +56 -6
  125. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  126. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +223 -33
  127. package/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.ts +559 -0
  128. package/Server/Utils/StartServer.ts +4 -3
  129. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +528 -0
  130. package/Server/Utils/TwoFactorBackupCodeNotification.ts +1 -0
  131. package/Server/Utils/UserPermission/UserPermission.ts +16 -0
  132. package/Server/Utils/Workflow/OAuth2TokenClient.ts +704 -0
  133. package/Server/Utils/Workflow/WorkflowVariableOAuthToken.ts +318 -0
  134. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +47 -7
  135. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +57 -16
  136. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +193 -214
  137. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +57 -13
  138. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.ts +54 -17
  139. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +179 -161
  140. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +281 -167
  141. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.ts +114 -0
  142. package/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.ts +895 -0
  143. package/Server/Utils/Workspace/Slack/Actions/ActionTypes.ts +7 -9
  144. package/Server/Utils/Workspace/Slack/Actions/Alert.ts +96 -227
  145. package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +93 -214
  146. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +30 -3
  147. package/Server/Utils/Workspace/Slack/Actions/Authorization.ts +99 -0
  148. package/Server/Utils/Workspace/Slack/Actions/Incident.ts +117 -283
  149. package/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.ts +101 -196
  150. package/Server/Utils/Workspace/Slack/Actions/ReactionNote.ts +329 -0
  151. package/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.ts +111 -287
  152. package/Server/Utils/Workspace/Slack/Slack.ts +86 -17
  153. package/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
  154. package/Server/Utils/Workspace/Workspace.ts +171 -54
  155. package/Server/Utils/Workspace/WorkspaceActionAuthorization.ts +252 -0
  156. package/Server/Utils/Workspace/WorkspaceBase.ts +39 -0
  157. package/Server/Utils/Workspace/WorkspaceOAuthState.ts +280 -0
  158. package/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.ts +96 -0
  159. package/Server/Utils/Workspace/WorkspaceReactionNote.ts +620 -0
  160. package/Tests/App/Dashboard/AttributesJSONDashboardSurfaces.test.tsx +496 -0
  161. package/Tests/App/Dashboard/AutoRemediationRulesTableCommandRunners.test.tsx +152 -0
  162. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +32 -0
  163. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  164. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  165. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  166. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  167. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +794 -0
  168. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  169. package/Tests/App/Dashboard/CreateOAuthWorkflowVariableModal.test.tsx +840 -0
  170. package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +653 -0
  171. package/Tests/App/Dashboard/DatabaseMonitorStepForm.test.tsx +197 -0
  172. package/Tests/App/Dashboard/DatabaseMonitorView.test.tsx +376 -0
  173. package/Tests/App/Dashboard/DisabledWarningRobustness.test.tsx +262 -0
  174. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  175. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  176. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  177. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  178. package/Tests/App/Dashboard/EventNotes.test.tsx +2080 -0
  179. package/Tests/App/Dashboard/EventNotesPages.test.tsx +486 -0
  180. package/Tests/App/Dashboard/EventNotesUtil.test.ts +933 -0
  181. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +238 -0
  182. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  183. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  184. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  185. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  186. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  187. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  188. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  189. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  190. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  191. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  192. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +665 -0
  193. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  194. package/Tests/App/Dashboard/IncidentEpisodeFeedPublicNoteNotifyDefault.test.tsx +494 -0
  195. package/Tests/App/Dashboard/IncidentEpisodeOverviewNotifyDefault.test.tsx +706 -0
  196. package/Tests/App/Dashboard/IncidentFeedPublicNoteNotifyDefault.test.tsx +472 -0
  197. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +858 -0
  198. package/Tests/App/Dashboard/IncomingEmailAddressSettings.test.tsx +567 -0
  199. package/Tests/App/Dashboard/InvestigationClusterToolFormat.test.ts +241 -0
  200. package/Tests/App/Dashboard/InvestigationEvidenceKubectl.test.tsx +322 -0
  201. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +12 -0
  202. package/Tests/App/Dashboard/InvestigationPanelClusterAccess.test.tsx +476 -0
  203. package/Tests/App/Dashboard/InvestigationRunDetailsKubectl.test.tsx +383 -0
  204. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  205. package/Tests/App/Dashboard/KubectlActivitySummary.test.tsx +366 -0
  206. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +2886 -0
  207. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +2688 -0
  208. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +915 -0
  209. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  210. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  211. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  212. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  213. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  214. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  215. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  216. package/Tests/App/Dashboard/LayerDateTimeFieldTimezoneName.test.tsx +66 -0
  217. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  218. package/Tests/App/Dashboard/LogMonitorPreview.test.tsx +174 -0
  219. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  220. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  221. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  222. package/Tests/App/Dashboard/MonitorFeedRefresh.test.tsx +279 -0
  223. package/Tests/App/Dashboard/MonitorMetricQueryConfig.test.ts +244 -0
  224. package/Tests/App/Dashboard/MonitorOpenWorkCard.test.tsx +381 -0
  225. package/Tests/App/Dashboard/MonitorOpenWorkHook.test.tsx +784 -0
  226. package/Tests/App/Dashboard/MonitorOverviewData.test.tsx +2199 -0
  227. package/Tests/App/Dashboard/MonitorOverviewHero.test.tsx +1169 -0
  228. package/Tests/App/Dashboard/MonitorOverviewInput.test.ts +1127 -0
  229. package/Tests/App/Dashboard/MonitorOverviewPage.test.tsx +2136 -0
  230. package/Tests/App/Dashboard/MonitorOverviewSelectReadable.test.ts +368 -0
  231. package/Tests/App/Dashboard/MonitorOverviewStatBar.test.tsx +526 -0
  232. package/Tests/App/Dashboard/MonitorOverviewSupportingCards.test.tsx +542 -0
  233. package/Tests/App/Dashboard/MonitorOwnersCombine.test.ts +299 -0
  234. package/Tests/App/Dashboard/MonitorProbesCard.test.tsx +602 -0
  235. package/Tests/App/Dashboard/MonitorResponseTimeCard.test.tsx +386 -0
  236. package/Tests/App/Dashboard/MonitorSetupAndConnectionCards.test.tsx +612 -0
  237. package/Tests/App/Dashboard/MonitorStatusChangesCard.test.tsx +257 -0
  238. package/Tests/App/Dashboard/MonitorSummaryTestMonitorButton.test.tsx +360 -1
  239. package/Tests/App/Dashboard/MonitorUptimeHistoryCard.test.tsx +835 -0
  240. package/Tests/App/Dashboard/MonitorUptimeSummaryHook.test.tsx +971 -0
  241. package/Tests/App/Dashboard/MonitorViewLayout.test.tsx +580 -0
  242. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  243. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  244. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  245. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  246. package/Tests/App/Dashboard/OnCallLayerRestrictionTimesPreserved.test.tsx +441 -0
  247. package/Tests/App/Dashboard/OnCallNavbar.test.tsx +214 -0
  248. package/Tests/App/Dashboard/OnCallSchedulePreviewCalendarDisplay.test.tsx +1389 -0
  249. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +205 -1
  250. package/Tests/App/Dashboard/OnCallScheduleTimeInputsSettingsZone.test.tsx +198 -0
  251. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  252. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  253. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  254. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +524 -0
  255. package/Tests/App/Dashboard/RemediationSuggestionCardApproval.test.tsx +702 -0
  256. package/Tests/App/Dashboard/RemoveUserFromProject.test.tsx +427 -0
  257. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +4 -0
  258. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  259. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  260. package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +3 -1
  261. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +166 -0
  262. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +622 -0
  263. package/Tests/App/Dashboard/ScheduledMaintenanceChangeStateNotifyDefault.test.tsx +1491 -0
  264. package/Tests/App/Dashboard/ScheduledMaintenanceFeedPublicNoteNotifyDefault.test.tsx +489 -0
  265. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewNotifyDefault.test.tsx +909 -0
  266. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +14 -0
  267. package/Tests/App/Dashboard/ScheduledMaintenanceWindowStatsTranslation.test.tsx +307 -0
  268. package/Tests/App/Dashboard/SecurityEventAttributeColumnPicker.test.tsx +542 -0
  269. package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +174 -0
  270. package/Tests/App/Dashboard/SecurityEventConnectionsEmptyState.test.tsx +973 -212
  271. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +491 -52
  272. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +301 -0
  273. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +158 -1
  274. package/Tests/App/Dashboard/SecurityEventProviderTile.test.tsx +869 -0
  275. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +959 -94
  276. package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +2 -0
  277. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +274 -1
  278. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  279. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  280. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  281. package/Tests/App/Dashboard/TeamMembershipRemoval.test.ts +900 -0
  282. package/Tests/App/Dashboard/TeamMembershipRemovalPages.test.tsx +460 -0
  283. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  284. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  285. package/Tests/App/Dashboard/TimezoneElements.test.tsx +321 -0
  286. package/Tests/App/Dashboard/TimezoneSelectButton.test.tsx +634 -0
  287. package/Tests/App/Dashboard/TraceExplorer.test.tsx +260 -0
  288. package/Tests/App/Dashboard/TraceMonitorPreview.test.tsx +182 -0
  289. package/Tests/App/Dashboard/UserTimezoneInit.test.tsx +544 -0
  290. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  291. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  292. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  293. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  294. package/Tests/App/Dashboard/WorkflowVariableModals.test.tsx +1630 -0
  295. package/Tests/App/Dashboard/WorkflowVariableRoutes.test.tsx +603 -0
  296. package/Tests/App/Dashboard/WorkflowVariableTokenStatus.test.tsx +589 -0
  297. package/Tests/App/Dashboard/WorkflowVariableUtil.test.ts +1543 -0
  298. package/Tests/App/Dashboard/WorkflowVariableView.test.tsx +3227 -0
  299. package/Tests/App/Dashboard/WorkflowVariablesTable.test.tsx +1277 -395
  300. package/Tests/App/StatusPage/StatusPageOverviewMonitorGroupUptime.test.tsx +714 -0
  301. package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.NewYork.test.tsx +1009 -0
  302. package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.test.tsx +1092 -0
  303. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +5 -0
  304. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  305. package/Tests/Models/DatabaseModels/KubernetesClusterAiAccessColumns.test.ts +520 -0
  306. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +66 -2
  307. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  308. package/Tests/Models/DatabaseModels/WorkspaceProjectAuthTokenAccessControl.test.ts +236 -0
  309. package/Tests/Models/IncidentEpisodePublicNoteNotifyColumnContract.test.ts +103 -0
  310. package/Tests/Models/IncidentPublicNoteNotifyColumnContract.test.ts +96 -0
  311. package/Tests/Models/ScheduledMaintenancePublicNoteNotifyColumnContract.test.ts +111 -0
  312. package/Tests/Server/API/AIInvestigationAPI.test.ts +976 -0
  313. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +836 -0
  314. package/Tests/Server/API/CommonAPIAssertCanCreateTable.test.ts +565 -0
  315. package/Tests/Server/API/CommonAPIAssertCanReadTable.test.ts +282 -0
  316. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +1744 -0
  317. package/Tests/Server/API/LogHistogramAPI.test.ts +371 -0
  318. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +125 -30
  319. package/Tests/Server/API/MicrosoftTeamsOAuthState.test.ts +1121 -0
  320. package/Tests/Server/API/MonitorUptimeSummaryAPI.test.ts +685 -0
  321. package/Tests/Server/API/SessionReplayAPI.test.ts +602 -1
  322. package/Tests/Server/API/SlackChannelCacheAPI.test.ts +358 -0
  323. package/Tests/Server/API/SlackOAuthState.test.ts +688 -0
  324. package/Tests/Server/API/StatusPageOverviewMonitorGroupUptime.test.ts +527 -0
  325. package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +2 -0
  326. package/Tests/Server/API/StatusPageUptimeAPI.test.ts +427 -0
  327. package/Tests/Server/API/UserProjectsAPI.test.ts +4 -0
  328. package/Tests/Server/API/WorkflowVariableAPI.test.ts +404 -0
  329. package/Tests/Server/API/WorkspaceOAuthTestHelpers.ts +289 -0
  330. package/Tests/Server/API/WorkspaceSendTestNotificationRoutes.test.ts +2064 -0
  331. package/Tests/Server/Infrastructure/GlobalCache.test.ts +86 -0
  332. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  333. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +589 -0
  334. package/Tests/Server/Infrastructure/Postgres/AddOAuth2WorkflowVariablesMigration.test.ts +254 -0
  335. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  336. package/Tests/Server/Middleware/BlockedUserMiddleware.test.ts +403 -0
  337. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +13 -0
  338. package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +2 -0
  339. package/Tests/Server/Middleware/RemovedProjectMemberAuthorization.test.ts +469 -0
  340. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +77 -1
  341. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +211 -0
  342. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +358 -0
  343. package/Tests/Server/Middleware/UserAuthorization.test.ts +97 -0
  344. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +2 -0
  345. package/Tests/Server/Services/AccessTokenProjectUserGrant.test.ts +14 -2
  346. package/Tests/Server/Services/AccessTokenRemovedMemberAccess.test.ts +1044 -0
  347. package/Tests/Server/Services/AddOwnersIdempotency.test.ts +5 -0
  348. package/Tests/Server/Services/AlertEpisodeMemberCreatePermission.test.ts +139 -0
  349. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +1361 -0
  350. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +375 -0
  351. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +2384 -0
  352. package/Tests/Server/Services/FindOwnersProjectMembership.test.ts +267 -0
  353. package/Tests/Server/Services/InboundEmail/SendGridInboundProviderAddress.test.ts +90 -0
  354. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  355. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  356. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  357. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  358. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  359. package/Tests/Server/Services/IncidentEpisodeMemberCreatePermission.test.ts +143 -0
  360. package/Tests/Server/Services/IncidentEpisodePublicNoteNotifyDefault.test.ts +805 -0
  361. package/Tests/Server/Services/IncidentPublicNoteNotifyDefault.test.ts +724 -0
  362. package/Tests/Server/Services/IncidentRoleProjectMembership.test.ts +233 -0
  363. package/Tests/Server/Services/KubernetesClusterAiAccessAgentRunnerAssignments.test.ts +372 -0
  364. package/Tests/Server/Services/KubernetesClusterAiAccessBindingGuard.test.ts +1876 -0
  365. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +620 -0
  366. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +421 -0
  367. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +3662 -0
  368. package/Tests/Server/Services/KubernetesClusterAiAccessSubjectClusters.test.ts +405 -0
  369. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +975 -0
  370. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +497 -0
  371. package/Tests/Server/Services/KubernetesClusterServiceDeleteSettlesRemediation.test.ts +328 -0
  372. package/Tests/Server/Services/LogAggregationService.test.ts +99 -0
  373. package/Tests/Server/Services/MonitorServiceIncomingEmailCustomAddress.test.ts +447 -0
  374. package/Tests/Server/Services/MonitorServiceProbeFlagSync.test.ts +1334 -0
  375. package/Tests/Server/Services/MonitorServiceProbeStatusGrouping.test.ts +1640 -0
  376. package/Tests/Server/Services/MonitorServiceRefreshCurrentStatus.test.ts +225 -0
  377. package/Tests/Server/Services/MonitorStatusBridgeProbePath.test.ts +8 -8
  378. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +264 -1
  379. package/Tests/Server/Services/MonitorStatusTimelineMergedDowntime.test.ts +284 -0
  380. package/Tests/Server/Services/MonitorUptimeSummaryService.test.ts +1043 -0
  381. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +5 -0
  382. package/Tests/Server/Services/OnCallUndeliverablePageOwnerMembership.test.ts +359 -0
  383. package/Tests/Server/Services/ProbeServiceDashboardLink.test.ts +191 -0
  384. package/Tests/Server/Services/ProjectOwnersProjectMembership.test.ts +221 -0
  385. package/Tests/Server/Services/RuleRun/IncidentAlertMaintenanceRuleEnginesRun.test.ts +5 -0
  386. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupARun.test.ts +28 -0
  387. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +9 -1
  388. package/Tests/Server/Services/RunnerJobEnqueueAiCommandAgentRunner.test.ts +230 -0
  389. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +978 -0
  390. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +866 -0
  391. package/Tests/Server/Services/RunnerJobSubmitResultRedaction.test.ts +322 -0
  392. package/Tests/Server/Services/RunnerJobSubmitResultUnstorableText.test.ts +357 -0
  393. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +227 -0
  394. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +581 -0
  395. package/Tests/Server/Services/ScheduledMaintenancePublicNoteNotifyDefault.test.ts +802 -0
  396. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +53 -0
  397. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +5 -0
  398. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +12 -0
  399. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +1176 -1
  400. package/Tests/Server/Services/TeamMemberBillingSeatSync.test.ts +9 -0
  401. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +18 -0
  402. package/Tests/Server/Services/TeamMemberProjectMembership.test.ts +255 -0
  403. package/Tests/Server/Services/TeamMemberRemovalRevokesProjectAccess.test.ts +600 -0
  404. package/Tests/Server/Services/TeamMemberResourceCleanup.test.ts +731 -0
  405. package/Tests/Server/Services/TeamMemberWorkspaceAccountCleanup.test.ts +164 -0
  406. package/Tests/Server/Services/TraceAggregationService.test.ts +64 -0
  407. package/Tests/Server/Services/UserServiceBlockedUser.test.ts +293 -0
  408. package/Tests/Server/Services/WorkflowVariableOAuthHooks.test.ts +1016 -0
  409. package/Tests/Server/Services/WorkflowVariableOAuthPostgres.test.ts +543 -0
  410. package/Tests/Server/Services/WorkflowVariableRenameUniqueness.test.ts +16 -5
  411. package/Tests/Server/Services/WorkspaceNotificationRuleNoteDelivery.test.ts +473 -0
  412. package/Tests/Server/Services/WorkspaceNotificationRuleSendTestNotification.test.ts +3690 -0
  413. package/Tests/Server/Services/WorkspaceProjectAuthTokenMiscDataSecrets.test.ts +374 -0
  414. package/Tests/Server/Services/WorkspaceProjectAuthTokenSlackChannelCache.test.ts +143 -0
  415. package/Tests/Server/Types/Database/Permissions/WorkflowVariableOAuthColumnPermission.test.ts +250 -0
  416. package/Tests/Server/Types/Database/QueryHelperJsonContains.test.ts +96 -0
  417. package/Tests/Server/Types/Markdown.test.ts +1033 -0
  418. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +285 -0
  419. package/Tests/Server/Utils/AI/CodeFix/CodeFixAgentAuth.test.ts +159 -1
  420. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +752 -0
  421. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +1840 -0
  422. package/Tests/Server/Utils/AI/KubectlJobRunner.test.ts +1067 -0
  423. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +355 -0
  424. package/Tests/Server/Utils/AI/PostIncidentStatusUpdateTool.test.ts +496 -0
  425. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +305 -0
  426. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +1365 -0
  427. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +639 -0
  428. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +1585 -0
  429. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +1065 -0
  430. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +2138 -1
  431. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +1035 -0
  432. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +517 -0
  433. package/Tests/Server/Utils/AI/ResourceTools.test.ts +27 -1
  434. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +1371 -24
  435. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorRollbackRace.test.ts +962 -0
  436. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +518 -0
  437. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +939 -2
  438. package/Tests/Server/Utils/Database/ProjectScopedRelationReferences.test.ts +371 -0
  439. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +64 -0
  440. package/Tests/Server/Utils/Monitor/DatabaseMetricObservationRemediation.test.ts +102 -0
  441. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +362 -0
  442. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +55 -0
  443. package/Tests/Server/Utils/Monitor/MonitorEmailAddressRedaction.test.ts +255 -0
  444. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +347 -0
  445. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +203 -0
  446. package/Tests/Server/Utils/Monitor/ProbeMonitorsNotification.test.ts +1147 -0
  447. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +79 -0
  448. package/Tests/Server/Utils/ResourceFeed/ResourceFeedUtil.test.ts +21 -1
  449. package/Tests/Server/Utils/Rules/OwnerRuleAssignment.test.ts +155 -1
  450. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  451. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +288 -0
  452. package/Tests/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.test.ts +304 -0
  453. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +40 -3
  454. package/Tests/Server/Utils/Workflow/OAuth2TokenClient.test.ts +702 -0
  455. package/Tests/Server/Utils/Workflow/OAuth2TokenClientHttpIntegration.test.ts +507 -0
  456. package/Tests/Server/Utils/Workflow/WorkflowVariableOAuthToken.test.ts +831 -0
  457. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +63 -1
  458. package/Tests/Server/Utils/Workspace/MicrosoftTeamsAppAccessToken.test.ts +256 -0
  459. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +60 -0
  460. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChatActionAuthorization.test.ts +442 -0
  461. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +44 -2
  462. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +55 -0
  463. package/Tests/Server/Utils/Workspace/MicrosoftTeamsReactionNoteSync.test.ts +1522 -0
  464. package/Tests/Server/Utils/Workspace/MicrosoftTeamsServiceUrl.test.ts +130 -0
  465. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +4 -4
  466. package/Tests/Server/Utils/Workspace/Slack/SlackActionAuthorization.test.ts +691 -0
  467. package/Tests/Server/Utils/Workspace/Slack/SlackMessageByTimestamp.test.ts +253 -0
  468. package/Tests/Server/Utils/Workspace/Slack/SlackReactionNote.test.ts +574 -0
  469. package/Tests/Server/Utils/Workspace/WorkspaceActionAuthorization.test.ts +488 -0
  470. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +861 -0
  471. package/Tests/Server/Utils/Workspace/WorkspaceMessageAuthor.test.ts +635 -0
  472. package/Tests/Server/Utils/Workspace/WorkspaceOAuthState.test.ts +548 -0
  473. package/Tests/Server/Utils/Workspace/WorkspaceReactionNote.test.ts +839 -0
  474. package/Tests/Server/Utils/Workspace/WorkspaceSendErrorMessage.test.ts +725 -0
  475. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +283 -9
  476. package/Tests/Types/DateBrowserLocalWallClock.test.ts +280 -0
  477. package/Tests/Types/DateLocalShortMonthNameFallback.test.ts +389 -0
  478. package/Tests/Types/DateUserTimezone.test.ts +177 -4
  479. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +306 -0
  480. package/Tests/Types/Kubernetes/KubernetesClusterAiAccess.test.ts +283 -0
  481. package/Tests/Types/Kubernetes/KubernetesRunnerPostureWriteScope.test.ts +68 -0
  482. package/Tests/Types/Monitor/DatabaseMonitor/SqlServerPlatform.test.ts +144 -0
  483. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +160 -0
  484. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +53 -0
  485. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  486. package/Tests/Types/Runbook/RunbookStepType.test.ts +91 -16
  487. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +14 -3
  488. package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +15 -0
  489. package/Tests/Types/StatusPage/PublicNoteSubscriberNotificationDefault.test.ts +1868 -0
  490. package/Tests/Types/Timezone.test.ts +5 -0
  491. package/Tests/Types/TimezoneAlias.test.ts +1034 -0
  492. package/Tests/Types/Workflow/JiraTemplateFixtures.ts +833 -0
  493. package/Tests/Types/Workflow/JiraTemplateScripts.test.ts +5414 -0
  494. package/Tests/Types/Workflow/JiraTemplateSimulation.test.ts +4803 -0
  495. package/Tests/Types/Workflow/JiraTemplates.test.ts +5429 -0
  496. package/Tests/Types/Workflow/Templates.test.ts +79 -4
  497. package/Tests/Types/Workflow/WorkflowVariableOAuth.test.ts +520 -0
  498. package/Tests/Types/Workspace/WorkspaceNoteReaction.test.ts +177 -0
  499. package/Tests/UI/Components/AttributesJSON/AttributesJSONView.test.tsx +390 -0
  500. package/Tests/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.test.tsx +566 -0
  501. package/Tests/UI/Components/AttributesJSON/JSONHighlight.test.ts +131 -0
  502. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +276 -1
  503. package/Tests/UI/Components/Calendar/Calendar.test.tsx +1611 -0
  504. package/Tests/UI/Components/Calendar/CalendarDayLayout.test.ts +1486 -0
  505. package/Tests/UI/Components/Calendar/CalendarDayLayoutDst.test.ts +632 -0
  506. package/Tests/UI/Components/Calendar/CalendarDst.test.tsx +388 -0
  507. package/Tests/UI/Components/Charts/ChartGroupPlotFloor.test.tsx +170 -0
  508. package/Tests/UI/Components/Charts/HistogramSelection.test.ts +282 -0
  509. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +6 -0
  510. package/Tests/UI/Components/CustomTimeRangeModalTimezoneTranslation.test.tsx +285 -0
  511. package/Tests/UI/Components/Dropdown.test.tsx +999 -2
  512. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  513. package/Tests/UI/Components/Forms/FormFieldCheckboxTranslation.test.tsx +216 -0
  514. package/Tests/UI/Components/Forms/FormFieldTimezoneDescriptionTranslation.test.tsx +461 -0
  515. package/Tests/UI/Components/Forms/IncidentEpisodePublicNoteNotifyPayload.test.tsx +641 -0
  516. package/Tests/UI/Components/Forms/IncidentPublicNoteNotifyPayload.test.tsx +346 -0
  517. package/Tests/UI/Components/Forms/ScheduledMaintenancePublicNoteNotifyPayload.test.tsx +493 -0
  518. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +911 -2
  519. package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.NewYork.test.tsx +730 -0
  520. package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.Tokyo.test.tsx +506 -0
  521. package/Tests/UI/Components/HistogramClickToZoom.test.tsx +619 -0
  522. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  523. package/Tests/UI/Components/LogDetailsPanelAttributesJSON.test.tsx +248 -0
  524. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +47 -1
  525. package/Tests/UI/Components/LogsViewerSmallScreens.test.tsx +620 -0
  526. package/Tests/UI/Components/ModelPageModelChange.test.tsx +242 -3
  527. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  528. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  529. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  530. package/Tests/UI/Components/ModelTable/BaseModelTableDeleteConfirmation.test.tsx +521 -0
  531. package/Tests/UI/Components/MonitorGraphs/Uptime.test.tsx +406 -2
  532. package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +169 -2
  533. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  534. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +266 -0
  535. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerSmallScreens.test.tsx +439 -0
  536. package/Tests/UI/Components/TimezoneDropdownLegacyValues.test.tsx +704 -0
  537. package/Tests/UI/Components/UseHistogramZoom.test.tsx +89 -1
  538. package/Tests/UI/Components/UseLogsHistogram.test.tsx +175 -0
  539. package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +5 -5
  540. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  541. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  542. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  543. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  544. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  545. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  546. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  547. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  548. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  549. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  550. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  551. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  552. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  553. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  554. package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +28 -0
  555. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  556. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  557. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  558. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +51 -0
  559. package/Tests/UI/Utils/Timezone.test.ts +747 -33
  560. package/Tests/UI/Workspace/SendTestNotificationButton.test.tsx +1854 -0
  561. package/Tests/UI/Workspace/WorkspaceCardsSendTestNotification.test.tsx +1864 -0
  562. package/Tests/Utils/AI/InvestigationReport.test.ts +435 -7
  563. package/Tests/Utils/AiRemediation/KubectlAllowlistValidatorParity.test.ts +1084 -0
  564. package/Tests/Utils/AiRemediation/KubectlOutputRedactor.test.ts +2531 -0
  565. package/Tests/Utils/AiRemediation/KubectlPolicy.test.ts +1895 -0
  566. package/Tests/Utils/AiRemediation/KubectlPolicyAllowlist.test.ts +428 -0
  567. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +828 -0
  568. package/Tests/Utils/AiRemediation/KubectlPolicyCustomResourceScope.test.ts +573 -0
  569. package/Tests/Utils/AiRemediation/KubectlPolicyDenials.test.ts +1525 -0
  570. package/Tests/Utils/AiRemediation/KubectlPolicyExposeOverrides.test.ts +275 -0
  571. package/Tests/Utils/AiRemediation/KubectlPolicyNodePatch.test.ts +428 -0
  572. package/Tests/Utils/AiRemediation/KubectlPolicyObjectWords.test.ts +195 -0
  573. package/Tests/Utils/AiRemediation/KubectlPolicyPatchParents.test.ts +637 -0
  574. package/Tests/Utils/AiRemediation/KubectlPolicyPodSecurityBaseline.test.ts +322 -0
  575. package/Tests/Utils/AiRemediation/KubectlPolicySafeWriteScope.test.ts +1026 -0
  576. package/Tests/Utils/AiRemediation/KubectlWaitBudget.test.ts +267 -0
  577. package/Tests/Utils/AiRemediation/KubectlWriteScope.test.ts +390 -0
  578. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +777 -0
  579. package/Tests/Utils/AiRemediation/KubectlWriteScopeClusterScoped.test.ts +613 -0
  580. package/Tests/Utils/AiRemediation/KubectlWriteScopeEntryPoint.test.ts +576 -0
  581. package/Tests/Utils/AiRemediation/KubectlWriteScopeKubectlSpellings.test.ts +752 -0
  582. package/Tests/Utils/AiRemediation/KubectlWriteScopeObjectWords.test.ts +358 -0
  583. package/Tests/Utils/AiRemediation/KubectlWriteScopeOverrides.test.ts +293 -0
  584. package/Tests/Utils/AiRemediation/KubectlWriteScopeParityCases.ts +598 -0
  585. package/Tests/Utils/AiRemediation/KubectlWriteScopePolicyParity.test.ts +403 -0
  586. package/Tests/Utils/Dashboard/DashboardClockComponent.test.ts +154 -5
  587. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  588. package/Tests/Utils/Monitor/IncomingEmailMonitorAddress.test.ts +478 -0
  589. package/Tests/Utils/Monitor/MonitorCheckScheduleUtil.test.ts +646 -0
  590. package/Tests/Utils/Monitor/MonitorOverviewCriteriaUtil.test.ts +292 -0
  591. package/Tests/Utils/Monitor/MonitorOverviewFamily.test.ts +274 -0
  592. package/Tests/Utils/Monitor/MonitorOverviewPresentationUtil.test.ts +3100 -0
  593. package/Tests/Utils/Monitor/MonitorOverviewProbeUtil.test.ts +1412 -0
  594. package/Tests/Utils/Monitor/MonitorOverviewTargetUtil.test.ts +374 -0
  595. package/Tests/Utils/Monitor/MonitorStatusHistoryUtil.test.ts +332 -0
  596. package/Tests/Utils/Monitor/MonitorUptimeSummaryUtil.test.ts +946 -0
  597. package/Tests/Utils/Rum/SessionIdentity.test.ts +94 -0
  598. package/Tests/Utils/Rum/SessionReplayWafCompatibility.test.ts +840 -0
  599. package/Tests/Utils/Rum/SessionReplayWireEncoding.test.ts +208 -0
  600. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  601. package/Tests/Utils/Rum/UserFlow.test.ts +1010 -0
  602. package/Tests/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.test.ts +217 -0
  603. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +1272 -1
  604. package/Tests/Utils/StatusPage/UptimeDailyAggregateTotals.test.ts +298 -0
  605. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +649 -1
  606. package/Tests/Utils/StatusPage/UptimeDailyAggregateWindow.test.ts +314 -0
  607. package/Tests/Utils/Telemetry/AttributesJSON.test.ts +454 -0
  608. package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +418 -2
  609. package/Tests/Utils/Uptime/DayUptimeGraphUtilMidnightDst.test.ts +160 -0
  610. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Auckland.test.ts +45 -0
  611. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Kolkata.test.ts +40 -0
  612. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.London.test.ts +147 -0
  613. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.NewYork.test.ts +48 -0
  614. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Tokyo.test.ts +39 -0
  615. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezoneCases.ts +1015 -0
  616. package/Tests/Utils/Uptime/UptimeUtil.test.ts +75 -0
  617. package/Tests/Utils/Uptime/UptimeUtilChronologicalOrder.test.ts +1279 -0
  618. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +203 -5
  619. package/Types/Date.ts +121 -7
  620. package/Types/Email/EmailMessage.ts +8 -0
  621. package/Types/Email/EmailTemplateType.ts +1 -0
  622. package/Types/Exception/ExceptionMessages.ts +1 -0
  623. package/Types/Incident/IncidentAlertLink.ts +38 -0
  624. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +488 -0
  625. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +54 -0
  626. package/Types/Kubernetes/KubernetesClusterAiAccessToolNames.ts +17 -0
  627. package/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.ts +9 -0
  628. package/Types/Monitor/DatabaseMonitor/SqlServerPlatform.ts +117 -0
  629. package/Types/Monitor/KubernetesAlertTemplates.ts +6 -5
  630. package/Types/Monitor/MonitorStepExceptionMonitor.ts +56 -0
  631. package/Types/Monitor/MonitorUptimeSummary.ts +113 -0
  632. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +11 -0
  633. package/Types/Permission.ts +48 -0
  634. package/Types/Rum/SessionReplay.ts +80 -8
  635. package/Types/Rum/SessionReplayApi.ts +44 -1
  636. package/Types/Rum/UserFlow.ts +187 -0
  637. package/Types/Rum/WebVitals.ts +52 -0
  638. package/Types/Runbook/RunbookStepType.ts +10 -0
  639. package/Types/Runbook/RunnerJobOrigin.ts +15 -0
  640. package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +6 -2
  641. package/Types/StatusPage/MergedDowntimeTotals.ts +17 -0
  642. package/Types/StatusPage/PublicNoteSubscriberNotificationDefault.ts +148 -0
  643. package/Types/StatusPage/UptimeDailyAggregate.ts +15 -0
  644. package/Types/Telemetry/TelemetryIngestSurface.ts +9 -0
  645. package/Types/Timezone.ts +5 -0
  646. package/Types/TimezoneAlias.ts +385 -0
  647. package/Types/Workflow/Templates.ts +3041 -2
  648. package/Types/Workflow/WorkflowVariableOAuth.ts +476 -0
  649. package/Types/Workspace/WorkspaceNoteReaction.ts +132 -0
  650. package/Typings/Index.d.ts +24 -0
  651. package/UI/Components/AttributesJSON/AttributesJSONPreferences.ts +175 -0
  652. package/UI/Components/AttributesJSON/AttributesJSONView.tsx +138 -0
  653. package/UI/Components/AttributesJSON/AttributesViewToggle.tsx +79 -0
  654. package/UI/Components/AttributesJSON/BracesGlyph.tsx +30 -0
  655. package/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.tsx +491 -0
  656. package/UI/Components/AttributesJSON/JSONHighlight.ts +77 -0
  657. package/UI/Components/Calendar/Calendar.css +83 -0
  658. package/UI/Components/Calendar/Calendar.tsx +136 -7
  659. package/UI/Components/Calendar/CalendarDayLayout.ts +236 -0
  660. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -1
  661. package/UI/Components/Charts/Utils/HistogramSelection.ts +117 -0
  662. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +269 -0
  663. package/UI/Components/Charts/Utils/useHistogramZoom.ts +26 -2
  664. package/UI/Components/Date/CustomTimeRangeModal.tsx +26 -3
  665. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  666. package/UI/Components/Dropdown/Dropdown.tsx +58 -9
  667. package/UI/Components/Forms/Fields/FormField.tsx +49 -14
  668. package/UI/Components/Graphs/DayUptimeGraph.tsx +166 -25
  669. package/UI/Components/Graphs/UptimeBarTooltip.tsx +10 -2
  670. package/UI/Components/Graphs/UptimeDaySummary.tsx +8 -0
  671. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  672. package/UI/Components/LogsViewer/LogsViewer.tsx +52 -5
  673. package/UI/Components/LogsViewer/components/ColumnSelector.tsx +13 -2
  674. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +13 -1
  675. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +91 -76
  676. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +15 -1
  677. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +28 -106
  678. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +54 -6
  679. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +8 -2
  680. package/UI/Components/LogsViewer/useLogsHistogram.ts +39 -6
  681. package/UI/Components/ModelTable/BaseModelTable.tsx +161 -31
  682. package/UI/Components/ModelTable/Column.ts +5 -0
  683. package/UI/Components/MonitorGraphs/Uptime.tsx +12 -0
  684. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +10 -2
  685. package/UI/Components/Page/ModelPage.tsx +59 -3
  686. package/UI/Components/Table/TableHeader.tsx +47 -2
  687. package/UI/Components/Table/Types/Column.ts +7 -0
  688. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +69 -5
  689. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +12 -1
  690. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +15 -1
  691. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +36 -104
  692. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +9 -1
  693. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  694. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +19 -1
  695. package/UI/Utils/PermissionGate.ts +11 -2
  696. package/UI/Utils/Timezone.ts +69 -28
  697. package/Utils/AI/InvestigationReport.ts +195 -25
  698. package/Utils/AiRemediation/KubectlOutputRedactor.ts +1724 -0
  699. package/Utils/AiRemediation/KubectlPolicy.ts +4136 -0
  700. package/Utils/AiRemediation/KubectlWaitBudget.ts +156 -0
  701. package/Utils/AiRemediation/KubectlWriteScope.ts +1964 -0
  702. package/Utils/Dashboard/Components/DashboardClockComponent.ts +7 -6
  703. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  704. package/Utils/Monitor/IncomingEmailMonitorAddress.ts +292 -0
  705. package/Utils/Monitor/MonitorCheckScheduleUtil.ts +654 -0
  706. package/Utils/Monitor/MonitorOverviewCriteriaUtil.ts +188 -0
  707. package/Utils/Monitor/MonitorOverviewFamily.ts +263 -0
  708. package/Utils/Monitor/MonitorOverviewPresentationUtil.ts +1958 -0
  709. package/Utils/Monitor/MonitorOverviewProbeUtil.ts +979 -0
  710. package/Utils/Monitor/MonitorOverviewTargetUtil.ts +299 -0
  711. package/Utils/Monitor/MonitorStatusHistoryUtil.ts +201 -0
  712. package/Utils/Monitor/MonitorUptimeSummaryUtil.ts +638 -0
  713. package/Utils/Rum/SessionIdentity.ts +44 -0
  714. package/Utils/Rum/SessionReplayWireEncoding.ts +137 -0
  715. package/Utils/Rum/SessionTraceState.ts +212 -0
  716. package/Utils/Rum/UserFlow.ts +1527 -0
  717. package/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.ts +107 -0
  718. package/Utils/StatusPage/ResourceUptime.ts +99 -0
  719. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +197 -0
  720. package/Utils/Telemetry/AttributesJSON.ts +367 -0
  721. package/Utils/Uptime/DayUptimeGraphUtil.ts +165 -0
  722. package/Utils/Uptime/UptimeUtil.ts +122 -38
  723. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +8 -1
  724. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
  725. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  726. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  727. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  728. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  729. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  730. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  731. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +8 -1
  732. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
  733. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +10 -3
  734. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  735. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  736. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  737. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +10 -3
  738. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  739. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  740. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  741. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +434 -0
  742. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  743. package/build/dist/Models/DatabaseModels/Monitor.js +49 -0
  744. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  745. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  746. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  747. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  748. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  749. package/build/dist/Models/DatabaseModels/Runner.js +4 -4
  750. package/build/dist/Models/DatabaseModels/Runner.js.map +1 -1
  751. package/build/dist/Models/DatabaseModels/RunnerJob.js +66 -0
  752. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  753. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +11 -3
  754. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  755. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +487 -3
  756. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  757. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js +64 -16
  758. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  759. package/build/dist/Server/API/AIInvestigationAPI.js +205 -0
  760. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  761. package/build/dist/Server/API/AutoRemediationAPI.js +115 -2
  762. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  763. package/build/dist/Server/API/CommonAPI.js +49 -0
  764. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  765. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +564 -0
  766. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -0
  767. package/build/dist/Server/API/MicrosoftTeamsAPI.js +465 -176
  768. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  769. package/build/dist/Server/API/MonitorAPI.js +107 -1
  770. package/build/dist/Server/API/MonitorAPI.js.map +1 -1
  771. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +5 -16
  772. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -1
  773. package/build/dist/Server/API/SlackAPI.js +322 -78
  774. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  775. package/build/dist/Server/API/StatusPageAPI.js +123 -17
  776. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  777. package/build/dist/Server/API/TelemetryAPI.js +144 -5
  778. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  779. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +1 -0
  780. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -1
  781. package/build/dist/Server/API/WorkflowVariableAPI.js +109 -0
  782. package/build/dist/Server/API/WorkflowVariableAPI.js.map +1 -0
  783. package/build/dist/Server/Infrastructure/GlobalCache.js +29 -0
  784. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  785. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js +38 -0
  786. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js.map +1 -0
  787. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js +54 -0
  788. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js.map +1 -0
  789. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js +14 -0
  790. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js.map +1 -0
  791. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js +12 -0
  792. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js.map +1 -0
  793. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js +32 -0
  794. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js.map +1 -0
  795. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  796. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  797. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  798. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  799. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
  800. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  801. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +21 -0
  802. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  803. package/build/dist/Server/Middleware/TelemetryIngest.js +32 -2
  804. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  805. package/build/dist/Server/Middleware/UserAuthorization.js +39 -6
  806. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  807. package/build/dist/Server/Services/AIAgentService.js +1 -0
  808. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  809. package/build/dist/Server/Services/AccessTokenService.js +87 -42
  810. package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
  811. package/build/dist/Server/Services/AlertEpisodeMemberService.js +4 -0
  812. package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
  813. package/build/dist/Server/Services/AlertEpisodeService.js +12 -1
  814. package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
  815. package/build/dist/Server/Services/AlertService.js +110 -23
  816. package/build/dist/Server/Services/AlertService.js.map +1 -1
  817. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +716 -30
  818. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  819. package/build/dist/Server/Services/AutoRemediationRuleService.js +100 -0
  820. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  821. package/build/dist/Server/Services/BillingService.js +1 -0
  822. package/build/dist/Server/Services/BillingService.js.map +1 -1
  823. package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js +11 -16
  824. package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js.map +1 -1
  825. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  826. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  827. package/build/dist/Server/Services/IncidentEpisodeMemberService.js +4 -0
  828. package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
  829. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +48 -0
  830. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  831. package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js +21 -0
  832. package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js.map +1 -1
  833. package/build/dist/Server/Services/IncidentEpisodeService.js +12 -1
  834. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  835. package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -0
  836. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  837. package/build/dist/Server/Services/IncidentMemberService.js +14 -0
  838. package/build/dist/Server/Services/IncidentMemberService.js.map +1 -1
  839. package/build/dist/Server/Services/IncidentPublicNoteService.js +48 -0
  840. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  841. package/build/dist/Server/Services/IncidentService.js +263 -33
  842. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  843. package/build/dist/Server/Services/IncidentStateTimelineService.js +33 -0
  844. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  845. package/build/dist/Server/Services/IncidentTemplateService.js +68 -4
  846. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  847. package/build/dist/Server/Services/Index.js +2 -0
  848. package/build/dist/Server/Services/Index.js.map +1 -1
  849. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +1470 -0
  850. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -0
  851. package/build/dist/Server/Services/KubernetesClusterService.js +1097 -0
  852. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  853. package/build/dist/Server/Services/LogAggregationService.js +14 -6
  854. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  855. package/build/dist/Server/Services/MonitorService.js +487 -48
  856. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  857. package/build/dist/Server/Services/MonitorStatusTimelineService.js +540 -3
  858. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  859. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -0
  860. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  861. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -0
  862. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
  863. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -0
  864. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  865. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +3 -0
  866. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  867. package/build/dist/Server/Services/OnCallNotificationAlertingService.js +29 -7
  868. package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -1
  869. package/build/dist/Server/Services/OnCallSetupReminderService.js +1 -0
  870. package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -1
  871. package/build/dist/Server/Services/ProbeService.js +7 -1
  872. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  873. package/build/dist/Server/Services/ProjectService.js +11 -1
  874. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  875. package/build/dist/Server/Services/RunbookCredentialService.js +24 -0
  876. package/build/dist/Server/Services/RunbookCredentialService.js.map +1 -1
  877. package/build/dist/Server/Services/RunbookSecretService.js +44 -0
  878. package/build/dist/Server/Services/RunbookSecretService.js.map +1 -1
  879. package/build/dist/Server/Services/RunnerJobService.js +641 -21
  880. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  881. package/build/dist/Server/Services/RunnerService.js +344 -5
  882. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  883. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +48 -0
  884. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  885. package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -20
  886. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  887. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js +67 -4
  888. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js.map +1 -1
  889. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +12 -1
  890. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  891. package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -0
  892. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  893. package/build/dist/Server/Services/StatusPageService.js +230 -32
  894. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  895. package/build/dist/Server/Services/StatusPageSubscriberService.js +4 -0
  896. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  897. package/build/dist/Server/Services/TeamMemberService.js +270 -11
  898. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  899. package/build/dist/Server/Services/TraceAggregationService.js +10 -6
  900. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  901. package/build/dist/Server/Services/UserEmailService.js +1 -0
  902. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  903. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1 -0
  904. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  905. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +1 -0
  906. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  907. package/build/dist/Server/Services/UserNotificationRuleService.js +4 -0
  908. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  909. package/build/dist/Server/Services/UserService.js +107 -1
  910. package/build/dist/Server/Services/UserService.js.map +1 -1
  911. package/build/dist/Server/Services/WorkflowVariableService.js +396 -7
  912. package/build/dist/Server/Services/WorkflowVariableService.js.map +1 -1
  913. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +463 -7
  914. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  915. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +144 -2
  916. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  917. package/build/dist/Server/Types/Database/QueryHelper.js +20 -0
  918. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  919. package/build/dist/Server/Types/Markdown.js +394 -27
  920. package/build/dist/Server/Types/Markdown.js.map +1 -1
  921. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +123 -0
  922. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -0
  923. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +344 -0
  924. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -0
  925. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +587 -0
  926. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -0
  927. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js +18 -2
  928. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js.map +1 -1
  929. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1380 -239
  930. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  931. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +1099 -68
  932. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  933. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +88 -6
  934. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  935. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +45 -11
  936. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  937. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +41 -11
  938. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  939. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +11 -4
  940. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
  941. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +4 -2
  942. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  943. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +997 -141
  944. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  945. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +299 -4
  946. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  947. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +120 -0
  948. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -0
  949. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +160 -1
  950. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  951. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +1 -0
  952. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  953. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +59 -12
  954. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  955. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +5 -1
  956. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  957. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +84 -22
  958. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  959. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +70 -5
  960. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -1
  961. package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js +249 -0
  962. package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js.map +1 -0
  963. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1 -0
  964. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -1
  965. package/build/dist/Server/Utils/PushNotificationUtil.js +27 -0
  966. package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -1
  967. package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js +26 -3
  968. package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js.map +1 -1
  969. package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js +38 -3
  970. package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js.map +1 -1
  971. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  972. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  973. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +143 -23
  974. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  975. package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js +406 -0
  976. package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js.map +1 -0
  977. package/build/dist/Server/Utils/StartServer.js +4 -3
  978. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  979. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +382 -0
  980. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -0
  981. package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js +1 -0
  982. package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js.map +1 -1
  983. package/build/dist/Server/Utils/UserPermission/UserPermission.js +10 -0
  984. package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
  985. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +447 -0
  986. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -0
  987. package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js +202 -0
  988. package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js.map +1 -0
  989. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +39 -1
  990. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  991. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +44 -4
  992. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  993. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +150 -191
  994. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  995. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +44 -4
  996. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  997. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js +39 -17
  998. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js.map +1 -1
  999. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +151 -145
  1000. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  1001. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +221 -127
  1002. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  1003. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js +91 -0
  1004. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js.map +1 -0
  1005. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js +673 -0
  1006. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js.map +1 -0
  1007. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js +3 -9
  1008. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js.map +1 -1
  1009. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +65 -166
  1010. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
  1011. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +63 -157
  1012. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -1
  1013. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +23 -3
  1014. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  1015. package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js +92 -0
  1016. package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js.map +1 -0
  1017. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +81 -210
  1018. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
  1019. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js +68 -138
  1020. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js.map +1 -1
  1021. package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js +240 -0
  1022. package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js.map +1 -0
  1023. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js +85 -212
  1024. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js.map +1 -1
  1025. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +60 -17
  1026. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  1027. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
  1028. package/build/dist/Server/Utils/Workspace/Workspace.js +134 -41
  1029. package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
  1030. package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js +197 -0
  1031. package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js.map +1 -0
  1032. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js +32 -0
  1033. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
  1034. package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js +199 -0
  1035. package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js.map +1 -0
  1036. package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js +91 -0
  1037. package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js.map +1 -0
  1038. package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js +450 -0
  1039. package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js.map +1 -0
  1040. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +103 -5
  1041. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  1042. package/build/dist/Types/Date.js +97 -7
  1043. package/build/dist/Types/Date.js.map +1 -1
  1044. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  1045. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  1046. package/build/dist/Types/Exception/ExceptionMessages.js +1 -0
  1047. package/build/dist/Types/Exception/ExceptionMessages.js.map +1 -1
  1048. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  1049. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  1050. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +245 -0
  1051. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -0
  1052. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +49 -0
  1053. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -0
  1054. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js +16 -0
  1055. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js.map +1 -0
  1056. package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js +104 -0
  1057. package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js.map +1 -0
  1058. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +6 -5
  1059. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  1060. package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js +32 -0
  1061. package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js.map +1 -1
  1062. package/build/dist/Types/Monitor/MonitorUptimeSummary.js +52 -0
  1063. package/build/dist/Types/Monitor/MonitorUptimeSummary.js.map +1 -0
  1064. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +9 -1
  1065. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  1066. package/build/dist/Types/Permission.js +42 -0
  1067. package/build/dist/Types/Permission.js.map +1 -1
  1068. package/build/dist/Types/Rum/SessionReplay.js +52 -2
  1069. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  1070. package/build/dist/Types/Rum/SessionReplayApi.js +11 -0
  1071. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  1072. package/build/dist/Types/Rum/UserFlow.js +115 -0
  1073. package/build/dist/Types/Rum/UserFlow.js.map +1 -0
  1074. package/build/dist/Types/Rum/WebVitals.js +25 -0
  1075. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  1076. package/build/dist/Types/Runbook/RunbookStepType.js +10 -0
  1077. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  1078. package/build/dist/Types/Runbook/RunnerJobOrigin.js +14 -0
  1079. package/build/dist/Types/Runbook/RunnerJobOrigin.js.map +1 -1
  1080. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +2 -2
  1081. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -1
  1082. package/build/dist/Types/StatusPage/MergedDowntimeTotals.js +2 -0
  1083. package/build/dist/Types/StatusPage/MergedDowntimeTotals.js.map +1 -0
  1084. package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js +65 -0
  1085. package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js.map +1 -0
  1086. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +8 -0
  1087. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  1088. package/build/dist/Types/Timezone.js +5 -0
  1089. package/build/dist/Types/Timezone.js.map +1 -1
  1090. package/build/dist/Types/TimezoneAlias.js +340 -0
  1091. package/build/dist/Types/TimezoneAlias.js.map +1 -0
  1092. package/build/dist/Types/Workflow/Templates.js +2694 -2
  1093. package/build/dist/Types/Workflow/Templates.js.map +1 -1
  1094. package/build/dist/Types/Workflow/WorkflowVariableOAuth.js +314 -0
  1095. package/build/dist/Types/Workflow/WorkflowVariableOAuth.js.map +1 -0
  1096. package/build/dist/Types/Workspace/WorkspaceNoteReaction.js +98 -0
  1097. package/build/dist/Types/Workspace/WorkspaceNoteReaction.js.map +1 -0
  1098. package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js +95 -0
  1099. package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js.map +1 -0
  1100. package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js +44 -0
  1101. package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js.map +1 -0
  1102. package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js +27 -0
  1103. package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js.map +1 -0
  1104. package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js +9 -0
  1105. package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js.map +1 -0
  1106. package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js +260 -0
  1107. package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js.map +1 -0
  1108. package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js +56 -0
  1109. package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js.map +1 -0
  1110. package/build/dist/UI/Components/Calendar/Calendar.js +94 -7
  1111. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  1112. package/build/dist/UI/Components/Calendar/CalendarDayLayout.js +136 -0
  1113. package/build/dist/UI/Components/Calendar/CalendarDayLayout.js.map +1 -0
  1114. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +1 -1
  1115. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  1116. package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js +72 -0
  1117. package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js.map +1 -0
  1118. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +178 -0
  1119. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -0
  1120. package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js +13 -1
  1121. package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js.map +1 -1
  1122. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +20 -4
  1123. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -1
  1124. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  1125. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  1126. package/build/dist/UI/Components/Dropdown/Dropdown.js +33 -9
  1127. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  1128. package/build/dist/UI/Components/Forms/Fields/FormField.js +36 -13
  1129. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  1130. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +117 -11
  1131. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  1132. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -1
  1133. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
  1134. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -1
  1135. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  1136. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  1137. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +36 -8
  1138. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  1139. package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js +10 -2
  1140. package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js.map +1 -1
  1141. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +9 -1
  1142. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
  1143. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +16 -15
  1144. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  1145. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  1146. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  1147. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +14 -74
  1148. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  1149. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +22 -5
  1150. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  1151. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +9 -2
  1152. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
  1153. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +16 -5
  1154. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -1
  1155. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +89 -22
  1156. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  1157. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +7 -1
  1158. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  1159. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +5 -1
  1160. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
  1161. package/build/dist/UI/Components/Page/ModelPage.js +37 -3
  1162. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  1163. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  1164. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  1165. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +27 -6
  1166. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  1167. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +7 -1
  1168. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
  1169. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  1170. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  1171. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +17 -77
  1172. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  1173. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +10 -1
  1174. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  1175. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  1176. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  1177. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +15 -1
  1178. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -1
  1179. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  1180. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  1181. package/build/dist/UI/Utils/Timezone.js +52 -17
  1182. package/build/dist/UI/Utils/Timezone.js.map +1 -1
  1183. package/build/dist/Utils/AI/InvestigationReport.js +116 -21
  1184. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  1185. package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js +1209 -0
  1186. package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js.map +1 -0
  1187. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +2973 -0
  1188. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -0
  1189. package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js +99 -0
  1190. package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js.map +1 -0
  1191. package/build/dist/Utils/AiRemediation/KubectlWriteScope.js +1429 -0
  1192. package/build/dist/Utils/AiRemediation/KubectlWriteScope.js.map +1 -0
  1193. package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js +7 -6
  1194. package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js.map +1 -1
  1195. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  1196. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  1197. package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js +214 -0
  1198. package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js.map +1 -0
  1199. package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js +471 -0
  1200. package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js.map +1 -0
  1201. package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js +107 -0
  1202. package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js.map +1 -0
  1203. package/build/dist/Utils/Monitor/MonitorOverviewFamily.js +191 -0
  1204. package/build/dist/Utils/Monitor/MonitorOverviewFamily.js.map +1 -0
  1205. package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js +1259 -0
  1206. package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js.map +1 -0
  1207. package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js +593 -0
  1208. package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js.map +1 -0
  1209. package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js +195 -0
  1210. package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js.map +1 -0
  1211. package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js +127 -0
  1212. package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js.map +1 -0
  1213. package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js +464 -0
  1214. package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js.map +1 -0
  1215. package/build/dist/Utils/Rum/SessionIdentity.js +31 -1
  1216. package/build/dist/Utils/Rum/SessionIdentity.js.map +1 -1
  1217. package/build/dist/Utils/Rum/SessionReplayWireEncoding.js +128 -0
  1218. package/build/dist/Utils/Rum/SessionReplayWireEncoding.js.map +1 -0
  1219. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  1220. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  1221. package/build/dist/Utils/Rum/UserFlow.js +941 -0
  1222. package/build/dist/Utils/Rum/UserFlow.js.map +1 -0
  1223. package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js +82 -0
  1224. package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js.map +1 -0
  1225. package/build/dist/Utils/StatusPage/ResourceUptime.js +46 -0
  1226. package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
  1227. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  1228. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -1
  1229. package/build/dist/Utils/Telemetry/AttributesJSON.js +245 -0
  1230. package/build/dist/Utils/Telemetry/AttributesJSON.js.map +1 -0
  1231. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +103 -0
  1232. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -1
  1233. package/build/dist/Utils/Uptime/UptimeUtil.js +91 -27
  1234. package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
  1235. package/jest.config.json +2 -1
  1236. package/package.json +2 -1
  1237. package/tsconfig.tests.json +36 -0
  1238. package/Tests/UI/Components/EmptyState/EmptyStateGuideLinks.test.tsx +0 -337
  1239. package/UI/Components/EmptyState/EmptyStateGuideLinks.tsx +0 -124
  1240. package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js +0 -47
  1241. 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
+ }