@oneuptime/common 14.0.9 → 14.0.10

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 (383) hide show
  1. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +60 -0
  2. package/Models/DatabaseModels/CephCluster.ts +213 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +213 -0
  4. package/Models/DatabaseModels/DockerHost.ts +213 -0
  5. package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
  6. package/Models/DatabaseModels/GlobalConfig.ts +1 -1
  7. package/Models/DatabaseModels/Host.ts +213 -0
  8. package/Models/DatabaseModels/Index.ts +2 -0
  9. package/Models/DatabaseModels/PodmanHost.ts +213 -0
  10. package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
  11. package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
  12. package/Models/DatabaseModels/RunnerJob.ts +144 -0
  13. package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
  14. package/Server/API/AutoRemediationAPI.ts +392 -1
  15. package/Server/API/ResourceAiAccessAPI.ts +1669 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  18. package/Server/Infrastructure/Semaphore.ts +22 -0
  19. package/Server/Middleware/TelemetryIngest.ts +15 -5
  20. package/Server/Services/AnalyticsDatabaseService.ts +45 -1
  21. package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
  22. package/Server/Services/CephClusterService.ts +109 -1
  23. package/Server/Services/DatabaseServerService.ts +116 -1
  24. package/Server/Services/DockerHostService.ts +109 -1
  25. package/Server/Services/DockerSwarmClusterService.ts +113 -1
  26. package/Server/Services/HostService.ts +126 -4
  27. package/Server/Services/MetricRecordingRuleService.ts +47 -0
  28. package/Server/Services/PodmanHostService.ts +109 -1
  29. package/Server/Services/ProxmoxClusterService.ts +110 -1
  30. package/Server/Services/ResourceAiAccessService.ts +1439 -0
  31. package/Server/Services/ResourceAiAgentJobService.ts +202 -0
  32. package/Server/Services/ResourceAiAgentService.ts +2432 -0
  33. package/Server/Services/RunnerJobService.ts +611 -4
  34. package/Server/Services/TelemetryUsageBillingService.ts +28 -0
  35. package/Server/Services/TraceRecordingRuleService.ts +33 -0
  36. package/Server/Services/VMwareVCenterService.ts +110 -1
  37. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1434 -32
  38. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +935 -21
  39. package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
  40. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
  41. package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
  42. package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
  43. package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
  44. package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
  45. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
  46. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +72 -2
  47. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
  48. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
  49. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +396 -13
  50. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
  51. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
  52. package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
  53. package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
  54. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
  55. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
  56. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
  57. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
  60. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +366 -143
  61. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
  63. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
  64. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
  65. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
  66. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
  67. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +93 -0
  68. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
  69. package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
  70. package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
  71. package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
  72. package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
  73. package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
  74. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
  75. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
  76. package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
  77. package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
  78. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
  79. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
  80. package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
  81. package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
  82. package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
  83. package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
  84. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
  85. package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
  86. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
  87. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +63 -5
  88. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +103 -5
  89. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +19 -1
  90. package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
  91. package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
  92. package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
  93. package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
  95. package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
  96. package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
  97. package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
  98. package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
  99. package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
  100. package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
  101. package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
  102. package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
  103. package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
  104. package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
  105. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +105 -0
  106. package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
  107. package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
  108. package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
  109. package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
  110. package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
  111. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
  112. package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
  113. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
  114. package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
  115. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
  116. package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
  117. package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
  118. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
  119. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
  120. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
  121. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
  122. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
  123. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
  124. package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
  125. package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
  126. package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
  127. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
  128. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +33 -3
  129. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
  130. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
  131. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
  132. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
  133. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
  134. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
  139. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
  140. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
  141. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
  142. package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
  143. package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
  144. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
  145. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
  146. package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
  147. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
  148. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
  149. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
  150. package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
  151. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
  152. package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
  153. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
  154. package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
  155. package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
  156. package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
  157. package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
  158. package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
  159. package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
  160. package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
  161. package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
  162. package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
  163. package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
  164. package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
  165. package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
  166. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
  167. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
  168. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
  169. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
  170. package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
  171. package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
  172. package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
  173. package/Types/AI/ResourceAiAccessApi.ts +140 -0
  174. package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
  175. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +183 -2
  176. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
  177. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
  178. package/Types/Monitor/RumAlertTemplates.ts +351 -4
  179. package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
  180. package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
  181. package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
  182. package/Types/Runbook/RunbookStepType.ts +29 -0
  183. package/Types/Telemetry/TelemetryIngestSurface.ts +14 -2
  184. package/Utils/AI/InvestigationReport.ts +121 -15
  185. package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
  186. package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
  187. package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
  188. package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
  189. package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
  190. package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
  191. package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
  192. package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
  193. package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
  194. package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
  195. package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
  196. package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
  197. package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
  198. package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
  199. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  200. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
  202. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
  204. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
  206. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
  208. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
  210. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  211. package/build/dist/Models/DatabaseModels/Host.js +219 -0
  212. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  214. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
  216. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
  218. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  219. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
  220. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
  221. package/build/dist/Models/DatabaseModels/RunnerJob.js +145 -0
  222. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  223. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
  224. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  225. package/build/dist/Server/API/AutoRemediationAPI.js +231 -1
  226. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  227. package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
  228. package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
  234. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  235. package/build/dist/Server/Middleware/TelemetryIngest.js +12 -5
  236. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  237. package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  239. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
  240. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  241. package/build/dist/Server/Services/CephClusterService.js +104 -0
  242. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  243. package/build/dist/Server/Services/DatabaseServerService.js +99 -0
  244. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
  245. package/build/dist/Server/Services/DockerHostService.js +104 -0
  246. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  247. package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
  248. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  249. package/build/dist/Server/Services/HostService.js +114 -2
  250. package/build/dist/Server/Services/HostService.js.map +1 -1
  251. package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
  252. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
  253. package/build/dist/Server/Services/PodmanHostService.js +104 -0
  254. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  255. package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
  256. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  257. package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
  258. package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
  259. package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
  260. package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
  261. package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
  262. package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
  263. package/build/dist/Server/Services/RunnerJobService.js +344 -5
  264. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  265. package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
  266. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  267. package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
  268. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
  269. package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
  270. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
  271. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1029 -30
  272. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  273. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +633 -28
  274. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  275. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
  276. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
  277. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
  278. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
  279. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
  280. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
  281. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
  282. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
  283. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
  284. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
  285. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
  286. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
  287. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
  288. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
  289. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +50 -4
  290. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  291. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
  292. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  293. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
  294. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  295. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +290 -13
  296. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  297. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
  298. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  299. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
  300. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  301. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
  302. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
  303. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
  304. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
  314. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  315. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
  316. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  317. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +263 -110
  318. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  319. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
  320. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
  321. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
  322. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
  323. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
  324. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
  325. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
  326. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
  327. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
  328. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
  329. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
  330. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
  331. package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
  332. package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
  333. package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
  334. package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
  335. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +116 -29
  336. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  337. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
  338. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  339. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
  340. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  341. package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
  342. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  343. package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
  344. package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
  345. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
  346. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
  347. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
  348. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
  349. package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
  350. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  351. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +13 -2
  352. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  353. package/build/dist/Utils/AI/InvestigationReport.js +71 -11
  354. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  355. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
  356. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
  357. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
  358. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
  359. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
  360. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
  361. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
  362. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
  363. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
  364. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
  365. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
  366. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
  367. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
  368. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
  369. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
  370. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
  371. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
  372. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
  373. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
  374. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
  375. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
  376. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
  377. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
  378. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
  379. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
  380. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
  381. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
  382. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
  383. package/package.json +1 -1
@@ -0,0 +1,2812 @@
1
+ /*
2
+ * Redacts credential-looking material from the output of a command a
3
+ * resource AI agent ran, before it leaves the agent, before the server
4
+ * stores it next to an AI run, and before a language model reads it.
5
+ *
6
+ * Two layers, applied in this order:
7
+ * 1. Per-program hooks (RESOURCE_OUTPUT_REDACTION_HOOKS) for material only
8
+ * one tool prints: every value of a `docker inspect` "Env" list, a Ceph
9
+ * keyring's `key`. Each tool's kit extends its program's entry.
10
+ * 2. The generic rules (GenericOutputRedactor): any `key: value` or
11
+ * `key=value` whose key looks credential-like, credential flags and
12
+ * their values, env name/value pairs, `scheme://user:password@`, Bearer
13
+ * and Basic tokens, JWTs, PEM private keys and long or padded base64.
14
+ *
15
+ * GenericOutputRedactor is a COPY of Utils/AiRemediation/
16
+ * KubectlOutputRedactor (everything from its KubectlOutputRedaction
17
+ * interface on), with only these renames: KubectlOutputRedactor ->
18
+ * GenericOutputRedactor (and `export default class` -> `export class`),
19
+ * KubectlOutputRedaction -> GenericOutputRedaction, KUBECTL_REDACTED_MARKER
20
+ * -> GENERIC_REDACTED_MARKER. It is copied, not imported, because this
21
+ * directory is import-closed (the resource AI agent carries a
22
+ * byte-identical copy of it); ResourceOutputRedactorCopyParity.test.ts
23
+ * fails the moment the two drift, so fix KubectlOutputRedactor and re-copy.
24
+ * Its Kubernetes-specific rules (data blocks, describe output,
25
+ * last-applied-configuration) are harmless on other tools' output.
26
+ *
27
+ * Over-masking is accepted; under-masking is not.
28
+ */
29
+
30
+ import AiResourceType from "../../../Types/ResourceAiAgent/AiResourceType";
31
+ import { redactDatabaseOutput } from "./DatabaseQueryRedactor";
32
+
33
+ export interface GenericOutputRedaction {
34
+ text: string;
35
+ // How many values, blocks or tokens were masked.
36
+ redactionCount: number;
37
+ }
38
+
39
+ export const GENERIC_REDACTED_MARKER: string = "[redacted]";
40
+
41
+ // Every marker this module writes starts with it: "[redacted-jwt]" too.
42
+ const REDACTED_PREFIX: string = "[redacted";
43
+
44
+ const DATA_BLOCK_KEYS: Set<string> = new Set<string>([
45
+ "data",
46
+ "stringData",
47
+ "binaryData",
48
+ ]);
49
+
50
+ const LAST_APPLIED_CONFIGURATION_ANNOTATION: string =
51
+ "kubectl.kubernetes.io/last-applied-configuration";
52
+
53
+ /*
54
+ * A key or env name that names credential material. Deliberately broad:
55
+ * `auth` also catches "authorization-mode", which is harmless to hide,
56
+ * while a narrow list would miss "DB_PASSWORD". A bare `key` is NOT
57
+ * credential-like (taints, tolerations and secretKeyRef all have one), but
58
+ * `tls.key`, `KEY_ID`, `api_key` and `private-key-file` are.
59
+ *
60
+ * `pass`, `pw` and `creds` count as a whole segment only — DB_PASS,
61
+ * RABBITMQ_DEFAULT_PASS, LDAP_BIND_PW, DB_CREDS, `--pass` — so "bypass",
62
+ * "compass", "passed", "MAX_PASSES" and "ssl-passthrough" stay readable,
63
+ * and "pass-through" is spelled out as not a credential. A few names glue
64
+ * a prefix on instead: Redis's `requirepass`, "dbpass", "smtppass".
65
+ */
66
+ const CREDENTIAL_NAME_REGEX: RegExp =
67
+ /passw(?:or)?d|passwd|pwd|passphrase|passcode|(?:^|[-_./])pass(?:$|[-_.](?!through))|(?:require|db|user|admin|root|smtp|mail|redis|ldap|bind|sasl|ftp)pass(?![a-z])|(?:^|[-_./])(?:pw|creds?)(?:$|[-_.])|secret|token|credential|auth|cert|bearer|cookie|dockerconfig|dockercfg|webhook|(?:api|access|private|priv|encryption|signing|client|ssh|master|license|service|account|session|app|consumer|shared)[-_.]?key|(?:^|[-_./])key[-_.]|[-_./]key$|(?:^|[-_./])(?:dsn|salt|otp)(?:[-_.]|$)|\.(?:crt|cer|pem|p12|pfx|jks)$/i;
68
+
69
+ /*
70
+ * Keys that match the pattern above but only ever REFER to a credential —
71
+ * the name of a Secret a volume mounts, whether a token is automounted —
72
+ * and that an on-call engineer needs when a pod cannot start. Compared
73
+ * lowercase, with leading dashes stripped so flag names match too.
74
+ */
75
+ const REFERENCE_ONLY_KEYS: Set<string> = new Set<string>([
76
+ "secretname",
77
+ "secretnamespace",
78
+ "secretproviderclass",
79
+ "secretkeyref",
80
+ "secretref",
81
+ "configmapkeyref",
82
+ "tokenexpirationseconds",
83
+ "expirationseconds",
84
+ "serviceaccounttoken",
85
+ "automountserviceaccounttoken",
86
+ "imagepullsecrets",
87
+ "image pull secrets",
88
+ "mountable secrets",
89
+ "tokens",
90
+ "audience",
91
+ "authorization-mode",
92
+ "anonymous-auth",
93
+ "tls-min-version",
94
+ "tls-cipher-suites",
95
+ "cert-manager.io/cluster-issuer",
96
+ "cert-manager.io/issuer",
97
+ ]);
98
+
99
+ /*
100
+ * Values that carry no material and read better untouched: an absent
101
+ * value, and describe's own "<set to the key 'x' in secret 'y'>" reference
102
+ * for an env entry that comes from a Secret.
103
+ */
104
+ const EXEMPT_VALUES: Set<string> = new Set<string>([
105
+ "",
106
+ '""',
107
+ "''",
108
+ "null",
109
+ "~",
110
+ "{}",
111
+ "[]",
112
+ "{",
113
+ "[",
114
+ "<none>",
115
+ "<nil>",
116
+ "<unset>",
117
+ "<invalid>",
118
+ "<empty>",
119
+ ]);
120
+
121
+ const EXEMPT_VALUE_PREFIX: string = "<set to the key ";
122
+
123
+ // YAML block scalar indicators: |, |-, |+, >, >-, >2 ...
124
+ const BLOCK_SCALAR_REGEX: RegExp = /^[|>][+-]?[0-9]*$/;
125
+
126
+ /*
127
+ * `key: value`, `key:` and `"key": value,` at the start of a line's content.
128
+ * Unquoted keys may contain spaces (describe prints "Image ID:") but never
129
+ * a colon, so "2026-09-22T10:00:00Z" and "http://…" are not keys. The colon
130
+ * must be followed by whitespace or end the line for the same reason.
131
+ */
132
+ const KEY_VALUE_LINE_REGEX: RegExp =
133
+ /^(?:"([^"]*)"|'([^']*)'|([^\s"'#:{}[\],][^:"']*?))[ \t]*:(?:[ \t]+|$)/;
134
+
135
+ /*
136
+ * An unquoted key with whitespace or `=` in it may be free text rather than
137
+ * a name: "connecting DB_PASSWORD=hunter2 host: db" parses as the key
138
+ * "connecting DB_PASSWORD=hunter2 host". Such a key is scanned for inline
139
+ * material too.
140
+ */
141
+ const FREE_TEXT_KEY_REGEX: RegExp = /[\s=]/;
142
+
143
+ const LIST_ITEM_PREFIX_REGEX: RegExp = /^-(?:[ \t]+|$)/;
144
+
145
+ const LEADING_WHITESPACE_REGEX: RegExp = /^[ \t]*/;
146
+
147
+ /*
148
+ * scheme://user:password@host — the password only; an empty user still
149
+ * counts. The match starts at "://" and the scheme is checked by a
150
+ * lookbehind, so the scan stays linear: a pattern that STARTS with the
151
+ * scheme retries `[a-z][a-z0-9+.-]*` from every word boundary of a long
152
+ * dotted run ("eyJ.eyJ.eyJ…", "a.b.c.…") and backtracks through the whole
153
+ * run each time — 200 KB of pod log took over 30 seconds on one thread.
154
+ */
155
+ const URL_CREDENTIAL_REGEX: RegExp =
156
+ /:\/\/(?<=\b[a-z][a-z0-9+.-]*:\/\/)([^\s/:@"']*):([^\s/@"']+)@/gi;
157
+
158
+ const BEARER_TOKEN_REGEX: RegExp = /\b(Bearer)[ \t]+([A-Za-z0-9._~+/-]{8,}=*)/g;
159
+
160
+ // Prose ("Basic configuration", "Bearer authentication") is lowercase letters only.
161
+ const LOWERCASE_WORD_REGEX: RegExp = /^[a-z]+$/;
162
+
163
+ const BASIC_TOKEN_REGEX: RegExp =
164
+ /\b(Basic)[ \t]+([A-Za-z0-9+/]{8,}={0,2})(?![A-Za-z0-9+/=])/g;
165
+
166
+ const JWT_REGEX: RegExp =
167
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/g;
168
+
169
+ /*
170
+ * PEM private key blocks. A block whose END line was truncated away is
171
+ * masked to the end of the output rather than left standing.
172
+ */
173
+ const PRIVATE_KEY_BLOCK_REGEX: RegExp =
174
+ /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----[\s\S]*?(?:-----END (?:[A-Z0-9]+ )*PRIVATE KEY-----|$)/g;
175
+
176
+ /*
177
+ * Compact JSON, as in a one-line `"data":{...}` — the quote may be escaped
178
+ * when the JSON itself sits inside a JSON string.
179
+ */
180
+ const INLINE_JSON_DATA_OBJECT_REGEX: RegExp =
181
+ /("|\\")(?:data|stringData|binaryData)(?:"|\\")\s*:\s*\{/g;
182
+
183
+ /*
184
+ * A JSON string body: anything but an unescaped quote. Two spellings, for
185
+ * plain JSON and for JSON escaped inside another JSON string.
186
+ */
187
+ const JSON_STRING_BODY: string = '(?:[^"\\\\]|\\\\.)*';
188
+ const ESCAPED_JSON_STRING_BODY: string = '(?:[^"\\\\]|\\\\[^"])*';
189
+ const JSON_QUOTE: string = '"';
190
+ const ESCAPED_JSON_QUOTE: string = '\\\\"';
191
+
192
+ function buildJsonEnvPairRegexes(isEscaped: boolean): {
193
+ nameFirst: RegExp;
194
+ valueFirst: RegExp;
195
+ } {
196
+ const body: string = isEscaped ? ESCAPED_JSON_STRING_BODY : JSON_STRING_BODY;
197
+ const q: string = isEscaped ? ESCAPED_JSON_QUOTE : JSON_QUOTE;
198
+
199
+ return {
200
+ nameFirst: new RegExp(
201
+ `(${q}name${q}\\s*:\\s*${q}(${body})${q}\\s*,\\s*${q}value${q}\\s*:\\s*${q})(${body})(${q})`,
202
+ "g",
203
+ ),
204
+ valueFirst: new RegExp(
205
+ `(${q}value${q}\\s*:\\s*${q})(${body})(${q}\\s*,\\s*${q}name${q}\\s*:\\s*${q}(${body})${q})`,
206
+ "g",
207
+ ),
208
+ };
209
+ }
210
+
211
+ const JSON_ENV_PAIR_REGEXES: Array<{ nameFirst: RegExp; valueFirst: RegExp }> =
212
+ [buildJsonEnvPairRegexes(false), buildJsonEnvPairRegexes(true)];
213
+
214
+ /*
215
+ * `"key":"value"` / `"key":123` in one-line JSON — jsonpath's `{.data}`, a
216
+ * structured log line. The key is judged by isCredentialKey. It never
217
+ * contains a quote, a backslash or a line break, which keeps the scan
218
+ * linear on a long escaped line. A string the output truncated before its
219
+ * closing quote runs to the end of the text; an object or array value is
220
+ * left for its own pairs. The only bare value JSON has that can hold
221
+ * material is a number, so prose such as `volume "certs" : secret "x"` is
222
+ * not read as a pair.
223
+ */
224
+ interface JsonCredentialPairRule {
225
+ quote: string;
226
+ regex: RegExp;
227
+ }
228
+
229
+ function buildJsonCredentialPairRule(
230
+ isEscaped: boolean,
231
+ ): JsonCredentialPairRule {
232
+ const body: string = isEscaped ? ESCAPED_JSON_STRING_BODY : JSON_STRING_BODY;
233
+ const q: string = isEscaped ? ESCAPED_JSON_QUOTE : JSON_QUOTE;
234
+
235
+ return {
236
+ quote: isEscaped ? '\\"' : '"',
237
+ regex: new RegExp(
238
+ `(${q}([^"\\\\\\n]{1,256})${q}\\s*:\\s*)(?:(${q})(${body})(${q}|$)|(-?[0-9][0-9.eE+-]*))`,
239
+ "g",
240
+ ),
241
+ };
242
+ }
243
+
244
+ const JSON_CREDENTIAL_PAIR_RULES: Array<JsonCredentialPairRule> = [
245
+ buildJsonCredentialPairRule(false),
246
+ buildJsonCredentialPairRule(true),
247
+ ];
248
+
249
+ /*
250
+ * `key=value` / `key: value` / `key:value` anywhere in a line — logs, args,
251
+ * Go-formatted maps (`map[password:cGFz…]`). The key is judged by
252
+ * isCredentialKey, so this never touches "timeout=1s" or "op=Exists". A
253
+ * JSON-escaped line break (`…\npassword: x\nuser: bob` inside a log
254
+ * message) separates pairs like a real one.
255
+ */
256
+ const INLINE_PAIR_REGEX: RegExp =
257
+ /(^|[\s[,{("']|\\[nrt])([A-Za-z0-9_./-]+)([ \t]*[:=][ \t]*)("(?:[^"\\]|\\.)*"|'[^']*'|(?:[^\s,;&\]})"'\\]|\\(?![nrt]))+)/g;
258
+
259
+ /*
260
+ * An image reference — `registry.example.com/auth:1.2.3`,
261
+ * `ghcr.io/org/secret-service:2.0` — has the shape of an inline pair whose
262
+ * key is a path and whose separator is a bare colon with no whitespace
263
+ * after it. A real pair with a path-shaped key (an annotation in describe
264
+ * output, `vault.hashicorp.com/agent-inject-token: abc123`) always has the
265
+ * space. The tag is what an on-call engineer needs most, so it stays.
266
+ */
267
+ function isImageReferencePair(key: string, separator: string): boolean {
268
+ return key.includes("/") && separator === ":";
269
+ }
270
+
271
+ /*
272
+ * A command-line flag standing alone as one argv element: "--db-password",
273
+ * "-password". A credential flag's value is then the NEXT element, which
274
+ * the pair rules above never see.
275
+ */
276
+ const FLAG_TOKEN_REGEX: RegExp = /^-{1,2}[A-Za-z][A-Za-z0-9_.-]*$/;
277
+
278
+ /*
279
+ * A flag's value inside one line: a quoted string, or a bare word that is
280
+ * neither the next flag ("-…") nor a marker this module already wrote.
281
+ */
282
+ const INLINE_FLAG_VALUE_PATTERN: RegExp =
283
+ /("(?:[^"\\]|\\.)*"|'[^']*'|[^\s"'[\-,;&|`\])}][^\s"',;&|`\])}]*)/;
284
+ const INLINE_FLAG_VALUE_SOURCE: string = INLINE_FLAG_VALUE_PATTERN.source;
285
+
286
+ // Where a flag can start inside a line.
287
+ const INLINE_FLAG_BOUNDARY_PATTERN: RegExp = /(^|[\s"'[(,;&|`])/;
288
+ const INLINE_FLAG_BOUNDARY_SOURCE: string = INLINE_FLAG_BOUNDARY_PATTERN.source;
289
+
290
+ /*
291
+ * `--requirepass value` / `-password value` in one line: a command in a
292
+ * log, describe's probe line, an `sh -c` script. The flag is judged by
293
+ * isCredentialKey, so "--port 8080" and "--tail 100" stay.
294
+ */
295
+ const INLINE_SPLIT_FLAG_REGEX: RegExp = new RegExp(
296
+ `${INLINE_FLAG_BOUNDARY_SOURCE}(--?[A-Za-z][A-Za-z0-9_.-]*)([ \\t]+)${INLINE_FLAG_VALUE_SOURCE}`,
297
+ "g",
298
+ );
299
+
300
+ /*
301
+ * `"--db-password","value"` — a flag and its value as neighbouring JSON
302
+ * array strings (jsonpath `{.spec.containers[*].args}`), plain, escaped
303
+ * inside another JSON string, or single-quoted as a Python list prints it.
304
+ */
305
+ function buildQuotedFlagPairRegex(quote: string, body: string): RegExp {
306
+ return new RegExp(
307
+ `(${quote}(--?[A-Za-z][A-Za-z0-9_.-]*)${quote}\\s*,\\s*${quote})(${body})(${quote})`,
308
+ "g",
309
+ );
310
+ }
311
+
312
+ const QUOTED_FLAG_PAIR_REGEXES: Array<RegExp> = [
313
+ buildQuotedFlagPairRegex(JSON_QUOTE, JSON_STRING_BODY),
314
+ buildQuotedFlagPairRegex(ESCAPED_JSON_QUOTE, ESCAPED_JSON_STRING_BODY),
315
+ buildQuotedFlagPairRegex("'", "[^']*"),
316
+ ];
317
+
318
+ /*
319
+ * Tools whose single-letter flag carries a password: `mysql -pS3cret`,
320
+ * `mongosh -p S3cret`, `sshpass -p S3cret`, `redis-cli -a S3cret`. The
321
+ * same letters mean something else everywhere else — `-p` is a port for
322
+ * ssh, psql and redis-cli and "make parents" for mkdir — so a short flag
323
+ * is judged only where the command names one of these tools: earlier on
324
+ * the same line, or earlier in the same one-element-per-line argv list.
325
+ */
326
+ const SHORT_PASSWORD_FLAG_BY_TOOL: Map<string, string> = new Map<
327
+ string,
328
+ string
329
+ >([
330
+ ["mysql", "p"],
331
+ ["mysqladmin", "p"],
332
+ ["mysqldump", "p"],
333
+ ["mysqlcheck", "p"],
334
+ ["mysqlimport", "p"],
335
+ ["mysqlshow", "p"],
336
+ ["mysqlpump", "p"],
337
+ ["mysqlsh", "p"],
338
+ ["mariadb", "p"],
339
+ ["mariadb-admin", "p"],
340
+ ["mariadb-dump", "p"],
341
+ ["mariadb-check", "p"],
342
+ ["mariadb-import", "p"],
343
+ ["mongo", "p"],
344
+ ["mongosh", "p"],
345
+ ["mongodump", "p"],
346
+ ["mongorestore", "p"],
347
+ ["mongoexport", "p"],
348
+ ["mongoimport", "p"],
349
+ ["mongostat", "p"],
350
+ ["mongotop", "p"],
351
+ ["sshpass", "p"],
352
+ ["redis-cli", "a"],
353
+ ["keydb-cli", "a"],
354
+ ["valkey-cli", "a"],
355
+ ]);
356
+
357
+ // One of those tools named inside a line, a path in front of it allowed.
358
+ const TOOL_NAME_REGEX: RegExp = new RegExp(
359
+ `(^|[\\s/"'[(,;&|=\`])(${Array.from(SHORT_PASSWORD_FLAG_BY_TOOL.keys()).join("|")})(?=$|[\\s"'\\]),;&|\`\\\\])`,
360
+ "g",
361
+ );
362
+
363
+ // Where the command a tool starts ends inside a line.
364
+ const COMMAND_END_REGEX: RegExp = /[\];|&)`]/;
365
+
366
+ interface ShortPasswordFlagRegexes {
367
+ // "-pS3cret"
368
+ attached: RegExp;
369
+ // "-p S3cret", and "-p","S3cret" in a JSON array.
370
+ separated: RegExp;
371
+ }
372
+
373
+ // The password glued to a short flag ("-pS3cret"): a bare word.
374
+ const SHORT_PASSWORD_ATTACHED_VALUE_PATTERN: RegExp =
375
+ /([^\s"'[\-,;&|`\])}][^\s"',;&|`\])}]*)/;
376
+
377
+ // What separates a short flag from its password: blanks, or a list's ",".
378
+ const SHORT_PASSWORD_SEPARATOR_PATTERN: RegExp =
379
+ /([ \t]+|"\s*,\s*"|\\"\s*,\s*\\"|'\s*,\s*')/;
380
+
381
+ function buildShortPasswordFlagRegexes(
382
+ letter: string,
383
+ ): ShortPasswordFlagRegexes {
384
+ return {
385
+ attached: new RegExp(
386
+ `${INLINE_FLAG_BOUNDARY_SOURCE}(-${letter})${SHORT_PASSWORD_ATTACHED_VALUE_PATTERN.source}`,
387
+ "g",
388
+ ),
389
+ separated: new RegExp(
390
+ `${INLINE_FLAG_BOUNDARY_SOURCE}(-${letter})${SHORT_PASSWORD_SEPARATOR_PATTERN.source}${INLINE_FLAG_VALUE_SOURCE}`,
391
+ "g",
392
+ ),
393
+ };
394
+ }
395
+
396
+ const SHORT_PASSWORD_FLAG_REGEXES: Map<string, ShortPasswordFlagRegexes> =
397
+ new Map<string, ShortPasswordFlagRegexes>(
398
+ Array.from(new Set<string>(SHORT_PASSWORD_FLAG_BY_TOOL.values())).map(
399
+ (letter: string): [string, ShortPasswordFlagRegexes] => {
400
+ return [letter, buildShortPasswordFlagRegexes(letter)];
401
+ },
402
+ ),
403
+ );
404
+
405
+ /*
406
+ * `kubectl describe configmap` prints each data entry as "KEY:", this rule
407
+ * on the next line, and the value verbatim below it; the entries end with
408
+ * a "BinaryData" heading underlined the same way.
409
+ */
410
+ const DESCRIBE_DATA_ENTRY_RULE: string = "----";
411
+ const DESCRIBE_BINARY_DATA_HEADING: string = "BinaryData";
412
+ const DESCRIBE_SECTION_RULE: string = "====";
413
+
414
+ /*
415
+ * Base64 runs. A padded run of any length is base64 by construction
416
+ * ("cGFzc3dvcmQ=" is "password"); an unpadded run must be long and mixed to
417
+ * count, so identifiers, hashes and hex ids stay legible. The padding must
418
+ * end the token: "--feature-gates=Foo=true" is a flag, not a secret.
419
+ */
420
+ const BASE64_PADDED_REGEX: RegExp =
421
+ /(?<![A-Za-z0-9+/])([A-Za-z0-9+/]{4,})(={1,2})(?![A-Za-z0-9+/=])/g;
422
+
423
+ const BASE64_LONG_REGEX: RegExp =
424
+ /(?<![A-Za-z0-9+/])([A-Za-z0-9+/]{32,})(?![A-Za-z0-9+/=])/g;
425
+
426
+ interface ParsedLine {
427
+ raw: string;
428
+ isBlank: boolean;
429
+ // Width of the leading whitespace.
430
+ indent: number;
431
+ // A YAML sequence entry: "- key: value" or "- value".
432
+ isListItem: boolean;
433
+ // Where the entry's own content starts: after the "- " of a list item.
434
+ contentIndent: number;
435
+ // The key of a "key: value" line, without quotes; absent otherwise.
436
+ key?: string | undefined;
437
+ // An unquoted key with whitespace or "=" in it, which may be free text.
438
+ keyIsFreeText?: boolean | undefined;
439
+ // Everything after the colon, trimmed; "" for "key:".
440
+ value?: string | undefined;
441
+ // Column in `raw` where the value text begins.
442
+ valueStart?: number | undefined;
443
+ }
444
+
445
+ interface RedactedLines {
446
+ lines: Array<string>;
447
+ // Index of the first line NOT consumed.
448
+ next: number;
449
+ count: number;
450
+ }
451
+
452
+ // What the up-front passes decided about lines the main pass reaches later.
453
+ interface LineMasks {
454
+ // `value:` lines of env entries whose name looks credential-like.
455
+ envValueLines: Set<number>;
456
+ // Argv elements that are a credential flag's value, with their masked text.
457
+ argvValueLines: Map<number, string>;
458
+ }
459
+
460
+ // A one-element-per-line argv list as findArgvValueLines walks it.
461
+ interface ArgvList {
462
+ indent: number;
463
+ isListItem: boolean;
464
+ // The short password flag of a tool named earlier in the list ("p", "a").
465
+ shortPasswordFlag: string | undefined;
466
+ // The previous element was a credential flag: this one is its value.
467
+ expectsValue: boolean;
468
+ }
469
+
470
+ function parseLine(raw: string): ParsedLine {
471
+ const whitespaceMatch: RegExpExecArray | null =
472
+ LEADING_WHITESPACE_REGEX.exec(raw);
473
+ const indent: number = whitespaceMatch ? whitespaceMatch[0].length : 0;
474
+ let content: string = raw.slice(indent);
475
+
476
+ if (content.trim() === "") {
477
+ return { raw, isBlank: true, indent, isListItem: false, contentIndent: 0 };
478
+ }
479
+
480
+ let isListItem: boolean = false;
481
+ let contentIndent: number = indent;
482
+
483
+ const listMatch: RegExpExecArray | null =
484
+ LIST_ITEM_PREFIX_REGEX.exec(content);
485
+ if (listMatch) {
486
+ isListItem = true;
487
+ contentIndent = indent + listMatch[0].length;
488
+ content = content.slice(listMatch[0].length);
489
+ }
490
+
491
+ const keyMatch: RegExpExecArray | null = KEY_VALUE_LINE_REGEX.exec(content);
492
+
493
+ if (!keyMatch) {
494
+ return { raw, isBlank: false, indent, isListItem, contentIndent };
495
+ }
496
+
497
+ const key: string = (keyMatch[1] ?? keyMatch[2] ?? keyMatch[3] ?? "").trim();
498
+ const valueStart: number = contentIndent + keyMatch[0].length;
499
+
500
+ return {
501
+ raw,
502
+ isBlank: false,
503
+ indent,
504
+ isListItem,
505
+ contentIndent,
506
+ key,
507
+ keyIsFreeText:
508
+ keyMatch[3] !== undefined && FREE_TEXT_KEY_REGEX.test(keyMatch[3]),
509
+ value: raw.slice(valueStart).trim(),
510
+ valueStart,
511
+ };
512
+ }
513
+
514
+ // Strip a JSON trailing comma and surrounding quotes from a scalar.
515
+ function unquote(value: string): string {
516
+ let body: string = value.trim();
517
+ if (body.endsWith(",")) {
518
+ body = body.slice(0, -1).trimEnd();
519
+ }
520
+ if (
521
+ body.length >= 2 &&
522
+ ((body.startsWith('"') && body.endsWith('"')) ||
523
+ (body.startsWith("'") && body.endsWith("'")))
524
+ ) {
525
+ body = body.slice(1, -1);
526
+ }
527
+ return body;
528
+ }
529
+
530
+ function isDocumentSeparator(line: ParsedLine): boolean {
531
+ return line.raw.trim() === "---";
532
+ }
533
+
534
+ function isBlockScalar(value: string): boolean {
535
+ return BLOCK_SCALAR_REGEX.test(value);
536
+ }
537
+
538
+ // "data:" or "\"data\": {" — a mapping whose entries follow on later lines.
539
+ function isContainerStart(value: string | undefined): boolean {
540
+ return value === "" || value === "{";
541
+ }
542
+
543
+ // A value with no material in it, including a marker an earlier pass wrote.
544
+ function isExemptValue(value: string): boolean {
545
+ let body: string = value.trim();
546
+ if (body.endsWith(",")) {
547
+ body = body.slice(0, -1).trimEnd();
548
+ }
549
+ return (
550
+ EXEMPT_VALUES.has(body) ||
551
+ body.startsWith(EXEMPT_VALUE_PREFIX) ||
552
+ unquote(body).startsWith(REDACTED_PREFIX)
553
+ );
554
+ }
555
+
556
+ function looksLikeCredentialName(name: string): boolean {
557
+ return CREDENTIAL_NAME_REGEX.test(name.trim());
558
+ }
559
+
560
+ // A key that names credential material, minus the reference-only keys.
561
+ function isCredentialKey(key: string): boolean {
562
+ const normalized: string = key.trim().replace(/^-+/, "").toLowerCase();
563
+ if (REFERENCE_ONLY_KEYS.has(normalized)) {
564
+ return false;
565
+ }
566
+ return looksLikeCredentialName(normalized);
567
+ }
568
+
569
+ /*
570
+ * An argv element that can be a flag's value: not the next flag, not
571
+ * empty, not a marker this module already wrote.
572
+ */
573
+ function isArgvValue(element: string): boolean {
574
+ return !element.startsWith("-") && !isExemptValue(element);
575
+ }
576
+
577
+ // The short password flag of the tool an argv element names, if any.
578
+ function shortPasswordFlagOfTool(element: string): string | undefined {
579
+ return SHORT_PASSWORD_FLAG_BY_TOOL.get(
580
+ element.slice(element.lastIndexOf("/") + 1),
581
+ );
582
+ }
583
+
584
+ /*
585
+ * A scalar replaced by the marker, keeping its quotes, a JSON trailing
586
+ * comma and `keptPrefix` (the "-p" of "-pS3cret").
587
+ */
588
+ function maskScalar(value: string, keptPrefix: string = ""): string {
589
+ const trailingComma: string = value.trimEnd().endsWith(",") ? "," : "";
590
+ const body: string = trailingComma
591
+ ? value.trimEnd().slice(0, -1).trimEnd()
592
+ : value.trim();
593
+
594
+ if (body.startsWith('"')) {
595
+ return `"${keptPrefix}${GENERIC_REDACTED_MARKER}"${trailingComma}`;
596
+ }
597
+ if (body.startsWith("'")) {
598
+ return `'${keptPrefix}${GENERIC_REDACTED_MARKER}'${trailingComma}`;
599
+ }
600
+ return `${keptPrefix}${GENERIC_REDACTED_MARKER}${trailingComma}`;
601
+ }
602
+
603
+ /*
604
+ * A bare value found inside a line, replaced by the marker. When the value
605
+ * ends the body of an escaped JSON string (`…=S3cret\"`), the backslash
606
+ * belongs to the closing quote and stays.
607
+ */
608
+ function maskInlineValue(value: string): string {
609
+ if (value.startsWith('"') || value.startsWith("'")) {
610
+ return maskScalar(value);
611
+ }
612
+ return `${GENERIC_REDACTED_MARKER}${value.endsWith("\\") ? "\\" : ""}`;
613
+ }
614
+
615
+ /*
616
+ * The part of a `key: value` line before its value. A key that may be free
617
+ * text gets the inline rules; an ordinary key is returned as it is.
618
+ */
619
+ function redactKeyPrefix(line: ParsedLine): { text: string; count: number } {
620
+ const prefix: string = line.raw.slice(0, line.valueStart ?? 0);
621
+
622
+ if (!line.keyIsFreeText) {
623
+ return { text: prefix, count: 0 };
624
+ }
625
+
626
+ const inline: { text: string; count: number } = applyInlineRules(
627
+ prefix.slice(line.contentIndent),
628
+ );
629
+
630
+ return {
631
+ text: prefix.slice(0, line.contentIndent) + inline.text,
632
+ count: inline.count,
633
+ };
634
+ }
635
+
636
+ function maskValueInPlace(line: ParsedLine): { text: string; count: number } {
637
+ if (line.valueStart === undefined || line.value === undefined) {
638
+ return {
639
+ text: " ".repeat(line.indent) + GENERIC_REDACTED_MARKER,
640
+ count: 1,
641
+ };
642
+ }
643
+
644
+ const prefix: { text: string; count: number } = redactKeyPrefix(line);
645
+
646
+ return {
647
+ text: prefix.text + maskScalar(line.value),
648
+ count: prefix.count + 1,
649
+ };
650
+ }
651
+
652
+ // The first line at or above `indent` — the end of a nested block.
653
+ function skipDeeperLines(
654
+ parsed: Array<ParsedLine>,
655
+ from: number,
656
+ indent: number,
657
+ ): number {
658
+ let index: number = from;
659
+ while (index < parsed.length) {
660
+ const line: ParsedLine = parsed[index]!;
661
+ if (!line.isBlank && line.indent <= indent) {
662
+ break;
663
+ }
664
+ index++;
665
+ }
666
+ return index;
667
+ }
668
+
669
+ const UPPERCASE_REGEX: RegExp = /[A-Z]/;
670
+ const LOWERCASE_REGEX: RegExp = /[a-z]/;
671
+ const DIGIT_REGEX: RegExp = /[0-9]/;
672
+
673
+ function hasUpperAndLower(text: string): boolean {
674
+ return UPPERCASE_REGEX.test(text) && LOWERCASE_REGEX.test(text);
675
+ }
676
+
677
+ function hasDigit(text: string): boolean {
678
+ return DIGIT_REGEX.test(text);
679
+ }
680
+
681
+ // `Bearer …` / `Basic …`: the token after the scheme, unless it is prose.
682
+ function maskAuthorizationTokens(
683
+ text: string,
684
+ regex: RegExp,
685
+ ): { text: string; count: number } {
686
+ let count: number = 0;
687
+
688
+ const result: string = text.replace(
689
+ regex,
690
+ (match: string, scheme: string, token: string): string => {
691
+ if (LOWERCASE_WORD_REGEX.test(token)) {
692
+ return match;
693
+ }
694
+ count++;
695
+ return `${scheme} ${GENERIC_REDACTED_MARKER}`;
696
+ },
697
+ );
698
+
699
+ return { text: result, count };
700
+ }
701
+
702
+ /*
703
+ * One-line JSON env pairs, `"name":"X","value":"Y"` in either order: the
704
+ * value is masked when the name looks credential-like and the value holds
705
+ * something (an empty value, or one already masked, stays as it is).
706
+ */
707
+ function maskJsonEnvPairs(
708
+ text: string,
709
+ regexes: { nameFirst: RegExp; valueFirst: RegExp },
710
+ ): { text: string; count: number } {
711
+ let count: number = 0;
712
+
713
+ let result: string = text.replace(
714
+ regexes.nameFirst,
715
+ (
716
+ match: string,
717
+ prefix: string,
718
+ name: string,
719
+ value: string,
720
+ closingQuote: string,
721
+ ): string => {
722
+ if (!looksLikeCredentialName(name) || isExemptValue(value)) {
723
+ return match;
724
+ }
725
+ count++;
726
+ return `${prefix}${GENERIC_REDACTED_MARKER}${closingQuote}`;
727
+ },
728
+ );
729
+
730
+ result = result.replace(
731
+ regexes.valueFirst,
732
+ (
733
+ match: string,
734
+ prefix: string,
735
+ value: string,
736
+ suffix: string,
737
+ name: string,
738
+ ): string => {
739
+ if (!looksLikeCredentialName(name) || isExemptValue(value)) {
740
+ return match;
741
+ }
742
+ count++;
743
+ return `${prefix}${GENERIC_REDACTED_MARKER}${suffix}`;
744
+ },
745
+ );
746
+
747
+ return { text: result, count };
748
+ }
749
+
750
+ /*
751
+ * One-line JSON `"key":"value"` pairs whose key looks credential-like, and
752
+ * the last-applied-configuration annotation as jsonpath prints it. The
753
+ * quotes stay, so the JSON still parses; a bare number becomes a string.
754
+ */
755
+ function maskJsonCredentialPairs(
756
+ text: string,
757
+ rule: JsonCredentialPairRule,
758
+ ): { text: string; count: number } {
759
+ let count: number = 0;
760
+
761
+ const result: string = text.replace(
762
+ rule.regex,
763
+ (
764
+ match: string,
765
+ prefix: string,
766
+ key: string,
767
+ openingQuote: string | undefined,
768
+ body: string | undefined,
769
+ closingQuote: string | undefined,
770
+ bare: string | undefined,
771
+ ): string => {
772
+ if (
773
+ key !== LAST_APPLIED_CONFIGURATION_ANNOTATION &&
774
+ !isCredentialKey(key)
775
+ ) {
776
+ return match;
777
+ }
778
+
779
+ if (openingQuote !== undefined) {
780
+ if (isExemptValue(body ?? "")) {
781
+ return match;
782
+ }
783
+ count++;
784
+ return `${prefix}${openingQuote}${GENERIC_REDACTED_MARKER}${closingQuote ?? ""}`;
785
+ }
786
+
787
+ if (bare === undefined) {
788
+ return match;
789
+ }
790
+ count++;
791
+ return `${prefix}${rule.quote}${GENERIC_REDACTED_MARKER}${rule.quote}`;
792
+ },
793
+ );
794
+
795
+ return { text: result, count };
796
+ }
797
+
798
+ // `"--db-password","value"` in a one-line array: the value is masked.
799
+ function maskQuotedFlagPairs(
800
+ text: string,
801
+ regex: RegExp,
802
+ ): { text: string; count: number } {
803
+ let count: number = 0;
804
+
805
+ const result: string = text.replace(
806
+ regex,
807
+ (
808
+ match: string,
809
+ prefix: string,
810
+ flag: string,
811
+ value: string,
812
+ closingQuote: string,
813
+ ): string => {
814
+ if (!isCredentialKey(flag) || !isArgvValue(value)) {
815
+ return match;
816
+ }
817
+ count++;
818
+ return `${prefix}${GENERIC_REDACTED_MARKER}${closingQuote}`;
819
+ },
820
+ );
821
+
822
+ return { text: result, count };
823
+ }
824
+
825
+ // `--requirepass value` inside one line: the value is masked.
826
+ function maskInlineSplitFlags(text: string): { text: string; count: number } {
827
+ let count: number = 0;
828
+
829
+ const result: string = text.replace(
830
+ INLINE_SPLIT_FLAG_REGEX,
831
+ (
832
+ match: string,
833
+ boundary: string,
834
+ flag: string,
835
+ separator: string,
836
+ value: string,
837
+ ): string => {
838
+ if (!isCredentialKey(flag) || !isArgvValue(unquote(value))) {
839
+ return match;
840
+ }
841
+ count++;
842
+ return `${boundary}${flag}${separator}${maskInlineValue(value)}`;
843
+ },
844
+ );
845
+
846
+ return { text: result, count };
847
+ }
848
+
849
+ // A tool's short password flag within the stretch of a line that is its command.
850
+ function maskShortPasswordFlag(
851
+ text: string,
852
+ letter: string,
853
+ ): { text: string; count: number } {
854
+ const regexes: ShortPasswordFlagRegexes | undefined =
855
+ SHORT_PASSWORD_FLAG_REGEXES.get(letter);
856
+ if (!regexes) {
857
+ return { text, count: 0 };
858
+ }
859
+
860
+ let count: number = 0;
861
+
862
+ let result: string = text.replace(
863
+ regexes.separated,
864
+ (
865
+ match: string,
866
+ boundary: string,
867
+ flag: string,
868
+ separator: string,
869
+ value: string,
870
+ ): string => {
871
+ if (!isArgvValue(unquote(value))) {
872
+ return match;
873
+ }
874
+ count++;
875
+ return `${boundary}${flag}${separator}${maskInlineValue(value)}`;
876
+ },
877
+ );
878
+
879
+ result = result.replace(
880
+ regexes.attached,
881
+ (_match: string, boundary: string, flag: string, value: string): string => {
882
+ count++;
883
+ return `${boundary}${flag}${maskInlineValue(value)}`;
884
+ },
885
+ );
886
+
887
+ return { text: result, count };
888
+ }
889
+
890
+ /*
891
+ * `mysql -pS3cret`, `mongosh -p S3cret`, `redis-cli -a S3cret`: a tool's
892
+ * short password flag, judged only in the stretch of the line from the
893
+ * tool's name to the next tool or the end of its command (`]`, `)`, `;`,
894
+ * `|`, `&`, a backtick).
895
+ */
896
+ function maskToolPasswordFlags(text: string): { text: string; count: number } {
897
+ const tools: Array<{ start: number; end: number; letter: string }> = [];
898
+
899
+ TOOL_NAME_REGEX.lastIndex = 0;
900
+ let match: RegExpExecArray | null = TOOL_NAME_REGEX.exec(text);
901
+ while (match) {
902
+ const start: number = match.index + (match[1] ?? "").length;
903
+ const name: string = match[2] ?? "";
904
+ tools.push({
905
+ start,
906
+ end: start + name.length,
907
+ letter: SHORT_PASSWORD_FLAG_BY_TOOL.get(name) ?? "",
908
+ });
909
+ match = TOOL_NAME_REGEX.exec(text);
910
+ }
911
+ TOOL_NAME_REGEX.lastIndex = 0;
912
+
913
+ if (tools.length === 0) {
914
+ return { text, count: 0 };
915
+ }
916
+
917
+ let result: string = "";
918
+ let count: number = 0;
919
+ let cursor: number = 0;
920
+
921
+ tools.forEach(
922
+ (
923
+ tool: { start: number; end: number; letter: string },
924
+ toolIndex: number,
925
+ ): void => {
926
+ /*
927
+ * Only the stretch up to the next tool is searched for the command's
928
+ * end, so a line naming a tool many times is still scanned once.
929
+ */
930
+ const nextTool: { start: number } | undefined = tools[toolIndex + 1];
931
+ const stretch: string = text.slice(
932
+ tool.end,
933
+ nextTool ? nextTool.start : text.length,
934
+ );
935
+ const commandEnd: number = stretch.search(COMMAND_END_REGEX);
936
+ const command: string =
937
+ commandEnd === -1 ? stretch : stretch.slice(0, commandEnd);
938
+
939
+ result += text.slice(cursor, tool.end);
940
+ const masked: { text: string; count: number } = maskShortPasswordFlag(
941
+ command,
942
+ tool.letter,
943
+ );
944
+ result += masked.text;
945
+ count += masked.count;
946
+ cursor = tool.end + command.length;
947
+ },
948
+ );
949
+
950
+ result += text.slice(cursor);
951
+
952
+ return { text: result, count };
953
+ }
954
+
955
+ /*
956
+ * Mask the body of every one-line `"data":{…}` object. Braces are matched
957
+ * by depth; an object the line truncated away is masked to the end.
958
+ */
959
+ function maskInlineJsonDataObjects(text: string): {
960
+ text: string;
961
+ count: number;
962
+ } {
963
+ let result: string = text;
964
+ let count: number = 0;
965
+ let searchFrom: number = 0;
966
+
967
+ while (searchFrom < result.length) {
968
+ INLINE_JSON_DATA_OBJECT_REGEX.lastIndex = searchFrom;
969
+ const match: RegExpExecArray | null =
970
+ INLINE_JSON_DATA_OBJECT_REGEX.exec(result);
971
+ if (!match) {
972
+ break;
973
+ }
974
+
975
+ const quote: string = match[1] ?? '"';
976
+ const bodyStart: number = match.index + match[0].length;
977
+ let depth: number = 1;
978
+ let position: number = bodyStart;
979
+
980
+ while (position < result.length && depth > 0) {
981
+ const character: string = result[position]!;
982
+ if (character === "{") {
983
+ depth++;
984
+ } else if (character === "}") {
985
+ depth--;
986
+ }
987
+ position++;
988
+ }
989
+
990
+ // `position` is just past the closing brace, or the end of the text.
991
+ const bodyEnd: number = depth === 0 ? position - 1 : result.length;
992
+
993
+ if (result.slice(bodyStart, bodyEnd).trim() === "") {
994
+ searchFrom = position;
995
+ continue;
996
+ }
997
+
998
+ const replacement: string = `${quote}${GENERIC_REDACTED_MARKER}${quote}:${quote}${GENERIC_REDACTED_MARKER}${quote}`;
999
+ result = result.slice(0, bodyStart) + replacement + result.slice(bodyEnd);
1000
+ count++;
1001
+ searchFrom = bodyStart + replacement.length;
1002
+ }
1003
+
1004
+ INLINE_JSON_DATA_OBJECT_REGEX.lastIndex = 0;
1005
+
1006
+ return { text: result, count };
1007
+ }
1008
+
1009
+ // The rules that apply to any line regardless of its YAML/JSON structure.
1010
+ function applyInlineRules(text: string): { text: string; count: number } {
1011
+ let count: number = 0;
1012
+ let result: string = text;
1013
+
1014
+ const apply: (masked: { text: string; count: number }) => void = (masked: {
1015
+ text: string;
1016
+ count: number;
1017
+ }): void => {
1018
+ result = masked.text;
1019
+ count += masked.count;
1020
+ };
1021
+
1022
+ result = result.replace(
1023
+ URL_CREDENTIAL_REGEX,
1024
+ (_match: string, user: string): string => {
1025
+ count++;
1026
+ return `://${user}:${GENERIC_REDACTED_MARKER}@`;
1027
+ },
1028
+ );
1029
+
1030
+ for (const tokenRegex of [BEARER_TOKEN_REGEX, BASIC_TOKEN_REGEX]) {
1031
+ apply(maskAuthorizationTokens(result, tokenRegex));
1032
+ }
1033
+
1034
+ result = result.replace(JWT_REGEX, (): string => {
1035
+ count++;
1036
+ return "[redacted-jwt]";
1037
+ });
1038
+
1039
+ apply(maskInlineJsonDataObjects(result));
1040
+
1041
+ for (const pair of JSON_ENV_PAIR_REGEXES) {
1042
+ apply(maskJsonEnvPairs(result, pair));
1043
+ }
1044
+
1045
+ for (const rule of JSON_CREDENTIAL_PAIR_RULES) {
1046
+ apply(maskJsonCredentialPairs(result, rule));
1047
+ }
1048
+
1049
+ for (const regex of QUOTED_FLAG_PAIR_REGEXES) {
1050
+ apply(maskQuotedFlagPairs(result, regex));
1051
+ }
1052
+
1053
+ result = result.replace(
1054
+ INLINE_PAIR_REGEX,
1055
+ (
1056
+ match: string,
1057
+ boundary: string,
1058
+ key: string,
1059
+ separator: string,
1060
+ value: string,
1061
+ ): string => {
1062
+ if (
1063
+ isImageReferencePair(key, separator) ||
1064
+ !isCredentialKey(key) ||
1065
+ isExemptValue(value)
1066
+ ) {
1067
+ return match;
1068
+ }
1069
+ count++;
1070
+ return `${boundary}${key}${separator}${GENERIC_REDACTED_MARKER}${value.endsWith("\\") ? "\\" : ""}`;
1071
+ },
1072
+ );
1073
+
1074
+ apply(maskInlineSplitFlags(result));
1075
+
1076
+ apply(maskToolPasswordFlags(result));
1077
+
1078
+ result = result.replace(
1079
+ BASE64_PADDED_REGEX,
1080
+ (match: string, run: string): string => {
1081
+ if (!hasUpperAndLower(run) && !hasDigit(run)) {
1082
+ return match;
1083
+ }
1084
+ count++;
1085
+ return "[redacted-base64]";
1086
+ },
1087
+ );
1088
+
1089
+ result = result.replace(
1090
+ BASE64_LONG_REGEX,
1091
+ (match: string, run: string): string => {
1092
+ if (!hasUpperAndLower(run) || !hasDigit(run)) {
1093
+ return match;
1094
+ }
1095
+ count++;
1096
+ return "[redacted-base64]";
1097
+ },
1098
+ );
1099
+
1100
+ return { text: result, count };
1101
+ }
1102
+
1103
+ /*
1104
+ * The inline rules over one line. A line that already parsed as
1105
+ * `key: value` has its value examined, and its key only when the key may
1106
+ * be free text: its key was judged by isCredentialKey (and possibly
1107
+ * exempted), and re-reading "Image pull secrets:" as the pair "secrets:
1108
+ * <none>" must not undo that.
1109
+ */
1110
+ function applyInlineRulesToLine(line: ParsedLine): {
1111
+ text: string;
1112
+ count: number;
1113
+ } {
1114
+ if (line.valueStart === undefined || line.value === undefined) {
1115
+ return applyInlineRules(line.raw);
1116
+ }
1117
+
1118
+ const prefix: { text: string; count: number } = redactKeyPrefix(line);
1119
+ const inline: { text: string; count: number } = applyInlineRules(
1120
+ line.raw.slice(line.valueStart),
1121
+ );
1122
+
1123
+ return {
1124
+ text: prefix.text + inline.text,
1125
+ count: prefix.count + inline.count,
1126
+ };
1127
+ }
1128
+
1129
+ export class GenericOutputRedactor {
1130
+ public static redact(output: string): GenericOutputRedaction {
1131
+ if (!output) {
1132
+ return { text: "", redactionCount: 0 };
1133
+ }
1134
+
1135
+ const parsed: Array<ParsedLine> = output
1136
+ .replace(/\r\n?/g, "\n")
1137
+ .split("\n")
1138
+ .map(parseLine);
1139
+
1140
+ const masks: LineMasks = {
1141
+ envValueLines: GenericOutputRedactor.findCredentialEnvValueLines(parsed),
1142
+ argvValueLines: GenericOutputRedactor.findArgvValueLines(parsed),
1143
+ };
1144
+
1145
+ const out: Array<string> = [];
1146
+ let count: number = 0;
1147
+ let index: number = 0;
1148
+
1149
+ while (index < parsed.length) {
1150
+ const line: ParsedLine = parsed[index]!;
1151
+
1152
+ let step: RedactedLines;
1153
+ if (
1154
+ GenericOutputRedactor.isDescribeDataEntry(parsed, index) &&
1155
+ isCredentialKey(line.key ?? "")
1156
+ ) {
1157
+ step = GenericOutputRedactor.redactDescribeDataEntry(parsed, index);
1158
+ } else if (
1159
+ !line.isBlank &&
1160
+ line.key !== undefined &&
1161
+ DATA_BLOCK_KEYS.has(line.key) &&
1162
+ isContainerStart(line.value)
1163
+ ) {
1164
+ step = GenericOutputRedactor.redactDataBlock(parsed, index, masks);
1165
+ } else {
1166
+ step = GenericOutputRedactor.redactStandaloneLine(parsed, index, masks);
1167
+ }
1168
+
1169
+ out.push(...step.lines);
1170
+ count += step.count;
1171
+ index = step.next;
1172
+ }
1173
+
1174
+ let text: string = out.join("\n");
1175
+
1176
+ text = text.replace(PRIVATE_KEY_BLOCK_REGEX, (): string => {
1177
+ count++;
1178
+ return "[redacted-private-key]";
1179
+ });
1180
+
1181
+ return { text, redactionCount: count };
1182
+ }
1183
+
1184
+ /*
1185
+ * Which `value:` lines belong to an env entry whose `name:` looks
1186
+ * credential-like. Found up front because the value may precede the name
1187
+ * inside its entry, and the main pass only ever moves forward.
1188
+ */
1189
+ private static findCredentialEnvValueLines(
1190
+ parsed: Array<ParsedLine>,
1191
+ ): Set<number> {
1192
+ const valueLines: Set<number> = new Set<number>();
1193
+
1194
+ for (let index: number = 0; index < parsed.length; index++) {
1195
+ const line: ParsedLine = parsed[index]!;
1196
+
1197
+ if (line.isBlank || line.key !== "name" || line.value === undefined) {
1198
+ continue;
1199
+ }
1200
+
1201
+ const name: string = unquote(line.value);
1202
+ if (!name || !looksLikeCredentialName(name)) {
1203
+ continue;
1204
+ }
1205
+
1206
+ const valueLine: number | undefined =
1207
+ GenericOutputRedactor.findSiblingValueLine(parsed, index);
1208
+ if (valueLine !== undefined) {
1209
+ valueLines.add(valueLine);
1210
+ }
1211
+ }
1212
+
1213
+ return valueLines;
1214
+ }
1215
+
1216
+ /*
1217
+ * Argv printed one element per line — `- --db-password` / `- value` in
1218
+ * YAML, `"--db-password",` / `"value"` in pretty JSON, describe's Command
1219
+ * and Args blocks — and the elements that are the value of a credential
1220
+ * flag before them, each with its masked text. A list's elements are
1221
+ * consecutive lines at the same indent and of the same shape; a
1222
+ * `key: value` line ends it (describe prints an argument that contains
1223
+ * ": " as is, so a flag's value may look like one). A tool's short
1224
+ * password flag counts only once the list has named the tool.
1225
+ */
1226
+ private static findArgvValueLines(
1227
+ parsed: Array<ParsedLine>,
1228
+ ): Map<number, string> {
1229
+ const masked: Map<number, string> = new Map<number, string>();
1230
+ let list: ArgvList | undefined = undefined;
1231
+
1232
+ for (let index: number = 0; index < parsed.length; index++) {
1233
+ const line: ParsedLine = parsed[index]!;
1234
+
1235
+ if (line.isBlank) {
1236
+ continue;
1237
+ }
1238
+
1239
+ // "- key: value" is a mapping entry, never an argv element.
1240
+ if (line.isListItem && line.key !== undefined) {
1241
+ list = undefined;
1242
+ continue;
1243
+ }
1244
+
1245
+ if (
1246
+ list === undefined ||
1247
+ list.indent !== line.indent ||
1248
+ list.isListItem !== line.isListItem
1249
+ ) {
1250
+ list = {
1251
+ indent: line.indent,
1252
+ isListItem: line.isListItem,
1253
+ shortPasswordFlag: undefined,
1254
+ expectsValue: false,
1255
+ };
1256
+ }
1257
+
1258
+ const indentText: string = line.raw.slice(0, line.contentIndent);
1259
+ const content: string = line.raw.slice(line.contentIndent);
1260
+ const element: string = unquote(content);
1261
+
1262
+ if (list.expectsValue) {
1263
+ list.expectsValue = false;
1264
+ if (isArgvValue(element)) {
1265
+ masked.set(index, indentText + maskScalar(content));
1266
+ continue;
1267
+ }
1268
+ }
1269
+
1270
+ if (line.key !== undefined) {
1271
+ list = undefined;
1272
+ continue;
1273
+ }
1274
+
1275
+ const shortFlag: string | undefined =
1276
+ list.shortPasswordFlag !== undefined
1277
+ ? `-${list.shortPasswordFlag}`
1278
+ : undefined;
1279
+
1280
+ if (shortFlag !== undefined && element.startsWith(shortFlag)) {
1281
+ const attached: string = element.slice(shortFlag.length);
1282
+ if (attached === "") {
1283
+ list.expectsValue = true;
1284
+ } else if (isArgvValue(attached)) {
1285
+ masked.set(index, indentText + maskScalar(content, shortFlag));
1286
+ }
1287
+ continue;
1288
+ }
1289
+
1290
+ if (FLAG_TOKEN_REGEX.test(element) && isCredentialKey(element)) {
1291
+ list.expectsValue = true;
1292
+ continue;
1293
+ }
1294
+
1295
+ const toolFlag: string | undefined = shortPasswordFlagOfTool(element);
1296
+ if (toolFlag !== undefined) {
1297
+ list.shortPasswordFlag = toolFlag;
1298
+ }
1299
+ }
1300
+
1301
+ return masked;
1302
+ }
1303
+
1304
+ /*
1305
+ * The `value:` sibling of a `name:` line inside the same list entry (YAML)
1306
+ * or object (JSON). Siblings share the name line's content indent; a
1307
+ * shallower line ends the entry, a deeper one is nested under a sibling.
1308
+ */
1309
+ private static findSiblingValueLine(
1310
+ parsed: Array<ParsedLine>,
1311
+ nameIndex: number,
1312
+ ): number | undefined {
1313
+ const nameLine: ParsedLine = parsed[nameIndex]!;
1314
+ const indent: number = nameLine.contentIndent;
1315
+
1316
+ for (let index: number = nameIndex + 1; index < parsed.length; index++) {
1317
+ const line: ParsedLine = parsed[index]!;
1318
+ if (line.isBlank) {
1319
+ continue;
1320
+ }
1321
+ if (line.indent < indent) {
1322
+ break;
1323
+ }
1324
+ if (line.indent > indent || line.isListItem) {
1325
+ continue;
1326
+ }
1327
+ if (line.key === "value") {
1328
+ return index;
1329
+ }
1330
+ }
1331
+
1332
+ // "- name: X" is its entry's first line: nothing above it is a sibling.
1333
+ if (nameLine.isListItem) {
1334
+ return undefined;
1335
+ }
1336
+
1337
+ for (let index: number = nameIndex - 1; index >= 0; index--) {
1338
+ const line: ParsedLine = parsed[index]!;
1339
+ if (line.isBlank) {
1340
+ continue;
1341
+ }
1342
+ if (line.isListItem && line.contentIndent === indent) {
1343
+ // The entry's own "- …" line — a sibling, and the entry's start.
1344
+ return line.key === "value" ? index : undefined;
1345
+ }
1346
+ if (line.indent < indent) {
1347
+ break;
1348
+ }
1349
+ if (line.indent > indent || line.isListItem) {
1350
+ continue;
1351
+ }
1352
+ if (line.key === "value") {
1353
+ return index;
1354
+ }
1355
+ }
1356
+
1357
+ return undefined;
1358
+ }
1359
+
1360
+ /*
1361
+ * The `kind:` sibling of a data block: same indent, same object, on
1362
+ * either side of the block. Undefined when the output does not say — a
1363
+ * truncated object, or not a Kubernetes object at all.
1364
+ */
1365
+ private static findSiblingKind(
1366
+ parsed: Array<ParsedLine>,
1367
+ blockIndex: number,
1368
+ ): string | undefined {
1369
+ const indent: number = parsed[blockIndex]!.contentIndent;
1370
+
1371
+ for (let index: number = blockIndex - 1; index >= 0; index--) {
1372
+ const line: ParsedLine = parsed[index]!;
1373
+ if (line.isBlank) {
1374
+ continue;
1375
+ }
1376
+ if (isDocumentSeparator(line)) {
1377
+ break;
1378
+ }
1379
+ if (line.indent > indent) {
1380
+ continue;
1381
+ }
1382
+ if (line.isListItem && line.contentIndent === indent) {
1383
+ // The entry's own "- …" line: its first key, and the entry's start.
1384
+ if (line.key === "kind" && line.value !== undefined) {
1385
+ return unquote(line.value);
1386
+ }
1387
+ break;
1388
+ }
1389
+ if (line.indent < indent) {
1390
+ break;
1391
+ }
1392
+ if (line.isListItem) {
1393
+ continue;
1394
+ }
1395
+ if (line.key === "kind" && line.value !== undefined) {
1396
+ return unquote(line.value);
1397
+ }
1398
+ }
1399
+
1400
+ for (let index: number = blockIndex + 1; index < parsed.length; index++) {
1401
+ const line: ParsedLine = parsed[index]!;
1402
+ if (line.isBlank) {
1403
+ continue;
1404
+ }
1405
+ if (isDocumentSeparator(line)) {
1406
+ break;
1407
+ }
1408
+ if (line.indent > indent) {
1409
+ continue;
1410
+ }
1411
+ if (line.indent < indent) {
1412
+ break;
1413
+ }
1414
+ if (line.isListItem) {
1415
+ continue;
1416
+ }
1417
+ if (line.key === "kind" && line.value !== undefined) {
1418
+ return unquote(line.value);
1419
+ }
1420
+ }
1421
+
1422
+ return undefined;
1423
+ }
1424
+
1425
+ /*
1426
+ * A `kubectl describe configmap` data entry: "KEY:" with nothing after
1427
+ * the colon, then the "----" rule at the same indent on the next line.
1428
+ */
1429
+ private static isDescribeDataEntry(
1430
+ parsed: Array<ParsedLine>,
1431
+ index: number,
1432
+ ): boolean {
1433
+ const line: ParsedLine | undefined = parsed[index];
1434
+ const rule: ParsedLine | undefined = parsed[index + 1];
1435
+
1436
+ return (
1437
+ line !== undefined &&
1438
+ rule !== undefined &&
1439
+ !line.isBlank &&
1440
+ !line.isListItem &&
1441
+ line.key !== undefined &&
1442
+ line.value === "" &&
1443
+ !rule.isBlank &&
1444
+ rule.indent === line.indent &&
1445
+ rule.raw.trim() === DESCRIBE_DATA_ENTRY_RULE
1446
+ );
1447
+ }
1448
+
1449
+ /*
1450
+ * Where a describe data entry's value ends: at the next entry or the
1451
+ * "BinaryData" heading after the last one — each follows a blank line at
1452
+ * the entry's indent — or at the end of the output.
1453
+ */
1454
+ private static isDescribeDataValueEnd(
1455
+ parsed: Array<ParsedLine>,
1456
+ index: number,
1457
+ indent: number,
1458
+ ): boolean {
1459
+ const line: ParsedLine = parsed[index]!;
1460
+ const previous: ParsedLine | undefined = parsed[index - 1];
1461
+ const next: ParsedLine | undefined = parsed[index + 1];
1462
+
1463
+ if (line.isBlank || line.indent !== indent || !previous?.isBlank) {
1464
+ return false;
1465
+ }
1466
+
1467
+ if (GenericOutputRedactor.isDescribeDataEntry(parsed, index)) {
1468
+ return true;
1469
+ }
1470
+
1471
+ return (
1472
+ line.raw.trim() === DESCRIBE_BINARY_DATA_HEADING &&
1473
+ next !== undefined &&
1474
+ next.raw.trim() === DESCRIBE_SECTION_RULE
1475
+ );
1476
+ }
1477
+
1478
+ /*
1479
+ * A credential-like describe data entry. The key and the rule stay; the
1480
+ * whole value — every line of it, blank lines included, since a
1481
+ * properties file or a PEM block has them — becomes one marker, and the
1482
+ * blank lines that close the entry stay. A value the Runner truncated
1483
+ * is masked to the end of the output.
1484
+ */
1485
+ private static redactDescribeDataEntry(
1486
+ parsed: Array<ParsedLine>,
1487
+ headerIndex: number,
1488
+ ): RedactedLines {
1489
+ const header: ParsedLine = parsed[headerIndex]!;
1490
+ const valueStart: number = headerIndex + 2;
1491
+
1492
+ let end: number = valueStart;
1493
+ while (
1494
+ end < parsed.length &&
1495
+ !GenericOutputRedactor.isDescribeDataValueEnd(parsed, end, header.indent)
1496
+ ) {
1497
+ end++;
1498
+ }
1499
+
1500
+ let lastValueLine: number = valueStart - 1;
1501
+ for (let index: number = valueStart; index < end; index++) {
1502
+ if (!parsed[index]!.isBlank) {
1503
+ lastValueLine = index;
1504
+ }
1505
+ }
1506
+
1507
+ const lines: Array<string> = [header.raw, parsed[headerIndex + 1]!.raw];
1508
+ let count: number = 0;
1509
+
1510
+ if (lastValueLine >= valueStart) {
1511
+ const valueLines: Array<ParsedLine> = parsed.slice(
1512
+ valueStart,
1513
+ lastValueLine + 1,
1514
+ );
1515
+ const isAlreadyMasked: boolean = valueLines.every(
1516
+ (line: ParsedLine): boolean => {
1517
+ return line.isBlank || line.raw.trim() === GENERIC_REDACTED_MARKER;
1518
+ },
1519
+ );
1520
+
1521
+ if (isAlreadyMasked) {
1522
+ lines.push(
1523
+ ...valueLines.map((line: ParsedLine): string => {
1524
+ return line.raw;
1525
+ }),
1526
+ );
1527
+ } else {
1528
+ lines.push(" ".repeat(header.indent) + GENERIC_REDACTED_MARKER);
1529
+ count++;
1530
+ }
1531
+ }
1532
+
1533
+ for (let index: number = lastValueLine + 1; index < end; index++) {
1534
+ lines.push(parsed[index]!.raw);
1535
+ }
1536
+
1537
+ return { lines, next: end, count };
1538
+ }
1539
+
1540
+ /*
1541
+ * A `data:` / `stringData:` / `binaryData:` block. Every value is masked
1542
+ * for a Secret or an object of unknown kind; only credential-like keys
1543
+ * are masked for anything else. Keys are kept either way.
1544
+ */
1545
+ private static redactDataBlock(
1546
+ parsed: Array<ParsedLine>,
1547
+ blockIndex: number,
1548
+ masks: LineMasks,
1549
+ ): RedactedLines {
1550
+ const block: ParsedLine = parsed[blockIndex]!;
1551
+ const kind: string | undefined = GenericOutputRedactor.findSiblingKind(
1552
+ parsed,
1553
+ blockIndex,
1554
+ );
1555
+ const maskEveryValue: boolean =
1556
+ kind === undefined || kind.toLowerCase() === "secret";
1557
+
1558
+ const lines: Array<string> = [block.raw];
1559
+ let count: number = 0;
1560
+ let index: number = blockIndex + 1;
1561
+ let entryIndent: number | undefined = undefined;
1562
+ let entryMasked: boolean = false;
1563
+
1564
+ while (index < parsed.length) {
1565
+ const line: ParsedLine = parsed[index]!;
1566
+
1567
+ if (line.isBlank) {
1568
+ if (!entryMasked) {
1569
+ lines.push(line.raw);
1570
+ }
1571
+ index++;
1572
+ continue;
1573
+ }
1574
+
1575
+ if (line.indent <= block.contentIndent) {
1576
+ break;
1577
+ }
1578
+
1579
+ if (entryIndent === undefined) {
1580
+ entryIndent = line.indent;
1581
+ }
1582
+
1583
+ // Deeper than an entry: a block scalar body or nested structure.
1584
+ if (line.indent > entryIndent) {
1585
+ if (maskEveryValue || entryMasked) {
1586
+ index++;
1587
+ continue;
1588
+ }
1589
+ const nested: RedactedLines =
1590
+ GenericOutputRedactor.redactStandaloneLine(parsed, index, masks);
1591
+ lines.push(...nested.lines);
1592
+ count += nested.count;
1593
+ index = nested.next;
1594
+ continue;
1595
+ }
1596
+
1597
+ if (line.key === undefined) {
1598
+ // Not "key: value" — an unexpected shape inside a data block.
1599
+ if (maskEveryValue) {
1600
+ lines.push(" ".repeat(line.indent) + GENERIC_REDACTED_MARKER);
1601
+ count++;
1602
+ entryMasked = true;
1603
+ } else {
1604
+ const inline: { text: string; count: number } = applyInlineRules(
1605
+ line.raw,
1606
+ );
1607
+ lines.push(inline.text);
1608
+ count += inline.count;
1609
+ entryMasked = false;
1610
+ }
1611
+ index++;
1612
+ continue;
1613
+ }
1614
+
1615
+ if (maskEveryValue || isCredentialKey(line.key)) {
1616
+ if (line.value !== undefined && !isExemptValue(line.value)) {
1617
+ const masked: { text: string; count: number } =
1618
+ maskValueInPlace(line);
1619
+ lines.push(masked.text);
1620
+ count += masked.count;
1621
+ } else {
1622
+ lines.push(line.raw);
1623
+ }
1624
+ entryMasked = true;
1625
+ index++;
1626
+ continue;
1627
+ }
1628
+
1629
+ const inline: { text: string; count: number } =
1630
+ applyInlineRulesToLine(line);
1631
+ lines.push(inline.text);
1632
+ count += inline.count;
1633
+ entryMasked = false;
1634
+ index++;
1635
+ }
1636
+
1637
+ return { lines, next: index, count };
1638
+ }
1639
+
1640
+ /*
1641
+ * One line outside a data block: env value, argv value, credential-like
1642
+ * key, inline rules.
1643
+ */
1644
+ private static redactStandaloneLine(
1645
+ parsed: Array<ParsedLine>,
1646
+ index: number,
1647
+ masks: LineMasks,
1648
+ ): RedactedLines {
1649
+ const line: ParsedLine = parsed[index]!;
1650
+
1651
+ if (line.isBlank) {
1652
+ return { lines: [line.raw], next: index + 1, count: 0 };
1653
+ }
1654
+
1655
+ if (masks.envValueLines.has(index) && line.value !== undefined) {
1656
+ if (isBlockScalar(line.value)) {
1657
+ const masked: { text: string; count: number } = maskValueInPlace(line);
1658
+ return {
1659
+ lines: [masked.text],
1660
+ next: skipDeeperLines(parsed, index + 1, line.contentIndent),
1661
+ count: masked.count,
1662
+ };
1663
+ }
1664
+ if (!isExemptValue(line.value)) {
1665
+ const masked: { text: string; count: number } = maskValueInPlace(line);
1666
+ return { lines: [masked.text], next: index + 1, count: masked.count };
1667
+ }
1668
+ }
1669
+
1670
+ const argvValue: string | undefined = masks.argvValueLines.get(index);
1671
+ if (argvValue !== undefined) {
1672
+ return { lines: [argvValue], next: index + 1, count: 1 };
1673
+ }
1674
+
1675
+ if (
1676
+ line.key !== undefined &&
1677
+ line.value !== undefined &&
1678
+ (line.key === LAST_APPLIED_CONFIGURATION_ANNOTATION ||
1679
+ isCredentialKey(line.key))
1680
+ ) {
1681
+ if (isBlockScalar(line.value)) {
1682
+ const masked: { text: string; count: number } = maskValueInPlace(line);
1683
+ return {
1684
+ lines: [masked.text],
1685
+ next: skipDeeperLines(parsed, index + 1, line.contentIndent),
1686
+ count: masked.count,
1687
+ };
1688
+ }
1689
+ if (!isExemptValue(line.value)) {
1690
+ const masked: { text: string; count: number } = maskValueInPlace(line);
1691
+ return { lines: [masked.text], next: index + 1, count: masked.count };
1692
+ }
1693
+ /*
1694
+ * A credential-like key with nothing to hide on this line: its
1695
+ * children are judged on their own, and describe's "<set to the key
1696
+ * …>" reference must not be chewed up by the inline pair rule.
1697
+ */
1698
+ const prefix: { text: string; count: number } = redactKeyPrefix(line);
1699
+ return {
1700
+ lines: [prefix.text + line.raw.slice(line.valueStart ?? 0)],
1701
+ next: index + 1,
1702
+ count: prefix.count,
1703
+ };
1704
+ }
1705
+
1706
+ const inline: { text: string; count: number } =
1707
+ applyInlineRulesToLine(line);
1708
+ return { lines: [inline.text], next: index + 1, count: inline.count };
1709
+ }
1710
+ }
1711
+
1712
+ // The marker every masked value is replaced with.
1713
+ export const RESOURCE_REDACTED_MARKER: string = GENERIC_REDACTED_MARKER;
1714
+
1715
+ export type ResourceOutputRedaction = GenericOutputRedaction;
1716
+
1717
+ /*
1718
+ * One tool's extra redaction pass. It receives the raw output (before the
1719
+ * generic rules run) and returns the text with its tool-specific material
1720
+ * masked and how many values it masked. Must be total and pure.
1721
+ */
1722
+ export type ResourceOutputRedactionHook = (data: {
1723
+ resourceType: AiResourceType;
1724
+ program: string;
1725
+ text: string;
1726
+ }) => ResourceOutputRedaction;
1727
+
1728
+ /*
1729
+ * `"Env": [ ... ]` as docker inspect (and docker service inspect) prints
1730
+ * it, pretty-printed or compact, possibly escaped inside another JSON
1731
+ * string. Every entry's VALUE is masked — an environment is where
1732
+ * containers keep their secrets, and a name that does not look like one
1733
+ * (APP_CONFIG, DATABASE) holds one as often as not. Names stay, so the model
1734
+ * still knows which variables are set.
1735
+ */
1736
+ const DOCKER_ENV_LIST_REGEX: RegExp = /(\\?)"Env\1"\s*:\s*\[/g;
1737
+
1738
+ const FOUR_HEX_DIGITS_REGEX: RegExp = /^[0-9A-Fa-f]{4}$/;
1739
+
1740
+ // The character one escape sequence of an enclosing JSON string stands for.
1741
+ function decodeJsonEscape(
1742
+ text: string,
1743
+ index: number,
1744
+ ): {
1745
+ char: string;
1746
+ width: number;
1747
+ } {
1748
+ const next: string = text.charAt(index + 1);
1749
+
1750
+ if (
1751
+ next === "u" &&
1752
+ FOUR_HEX_DIGITS_REGEX.test(text.slice(index + 2, index + 6))
1753
+ ) {
1754
+ return {
1755
+ char: String.fromCharCode(parseInt(text.slice(index + 2, index + 6), 16)),
1756
+ width: 6,
1757
+ };
1758
+ }
1759
+
1760
+ return { char: next, width: 2 };
1761
+ }
1762
+
1763
+ /*
1764
+ * Where a JSON string literal's body ends: the index of its closing quote
1765
+ * (`"`, or `\"` when the literal is itself escaped one level inside another
1766
+ * JSON string), or text.length when it never closes.
1767
+ *
1768
+ * Escaped one level, the body is read through the enclosing string's
1769
+ * escapes: `\\\"` is the entry's own escaped quote (`\"` once decoded) and
1770
+ * `\\\\` its own escaped backslash, so neither is taken for the entry's end
1771
+ * — which would leave every entry after it unmasked. A bare `"` ends the
1772
+ * enclosing string, so the entry cannot go on past it either.
1773
+ */
1774
+ function readJsonStringEnd(
1775
+ text: string,
1776
+ start: number,
1777
+ isEscaped: boolean,
1778
+ ): number {
1779
+ let index: number = start;
1780
+
1781
+ if (!isEscaped) {
1782
+ while (index < text.length) {
1783
+ const ch: string = text.charAt(index);
1784
+
1785
+ if (ch === '"') {
1786
+ return index;
1787
+ }
1788
+
1789
+ index += ch === "\\" ? 2 : 1;
1790
+ }
1791
+
1792
+ return text.length;
1793
+ }
1794
+
1795
+ let innerEscape: boolean = false;
1796
+
1797
+ while (index < text.length) {
1798
+ const ch: string = text.charAt(index);
1799
+
1800
+ if (ch === '"') {
1801
+ return index;
1802
+ }
1803
+
1804
+ const decoded: { char: string; width: number } =
1805
+ ch === "\\" ? decodeJsonEscape(text, index) : { char: ch, width: 1 };
1806
+
1807
+ if (innerEscape) {
1808
+ innerEscape = false;
1809
+ } else if (decoded.char === "\\") {
1810
+ innerEscape = true;
1811
+ } else if (decoded.char === '"') {
1812
+ return index;
1813
+ }
1814
+
1815
+ index += decoded.width;
1816
+ }
1817
+
1818
+ return text.length;
1819
+ }
1820
+
1821
+ function maskDockerEnvValues(data: {
1822
+ resourceType: AiResourceType;
1823
+ program: string;
1824
+ text: string;
1825
+ }): ResourceOutputRedaction {
1826
+ const text: string = data.text;
1827
+ let out: string = "";
1828
+ let cursor: number = 0;
1829
+ let count: number = 0;
1830
+
1831
+ DOCKER_ENV_LIST_REGEX.lastIndex = 0;
1832
+ let match: RegExpExecArray | null = DOCKER_ENV_LIST_REGEX.exec(text);
1833
+
1834
+ while (match) {
1835
+ const isEscaped: boolean = match[1] === "\\";
1836
+ const quote: string = isEscaped ? '\\"' : '"';
1837
+ let index: number = match.index + match[0].length;
1838
+
1839
+ out += text.slice(cursor, index);
1840
+
1841
+ // Walk the list's string entries until its closing bracket (or the end).
1842
+ while (index < text.length) {
1843
+ const ch: string = text.charAt(index);
1844
+
1845
+ if (ch === "]") {
1846
+ break;
1847
+ }
1848
+
1849
+ if (!text.startsWith(quote, index)) {
1850
+ out += ch;
1851
+ index++;
1852
+ continue;
1853
+ }
1854
+
1855
+ const bodyStart: number = index + quote.length;
1856
+ const bodyEnd: number = readJsonStringEnd(text, bodyStart, isEscaped);
1857
+ const entry: string = text.slice(bodyStart, bodyEnd);
1858
+ const equals: number = entry.indexOf("=");
1859
+
1860
+ if (equals > 0 && equals < entry.length - 1) {
1861
+ out += `${quote}${entry.slice(0, equals + 1)}${RESOURCE_REDACTED_MARKER}`;
1862
+ count++;
1863
+ } else {
1864
+ out += `${quote}${entry}`;
1865
+ }
1866
+
1867
+ if (bodyEnd >= text.length || !text.startsWith(quote, bodyEnd)) {
1868
+ // The text ended, or (escaped) the enclosing string closed first.
1869
+ index = bodyEnd;
1870
+ break;
1871
+ }
1872
+
1873
+ out += quote;
1874
+ index = bodyEnd + quote.length;
1875
+ }
1876
+
1877
+ cursor = index;
1878
+ DOCKER_ENV_LIST_REGEX.lastIndex = Math.max(index, match.index + 1);
1879
+ match = DOCKER_ENV_LIST_REGEX.exec(text);
1880
+ }
1881
+
1882
+ out += text.slice(cursor);
1883
+
1884
+ return { text: out, redactionCount: count };
1885
+ }
1886
+
1887
+ /*
1888
+ * `docker service inspect --pretty` prints a service's environment on the
1889
+ * " Env:" line of its ContainerSpec section, where the JSON hook above
1890
+ * cannot see it:
1891
+ * " Env:\t\tAPP_KEY=abc LOG_LEVEL=debug JAVA_OPTS=-Xmx1g -Dx=y "
1892
+ * The CLI's template prints each entry raw, so a value holding a newline (a
1893
+ * PEM certificate, a JSON blob from a YAML block) carries the rest of the
1894
+ * list onto the next physical lines. Inside a ContainerSpec section the Env
1895
+ * block therefore runs until the next line the template itself starts
1896
+ * (" Dir:", " Init:", " User:", "Mounts:", ... and "Endpoint Mode:", which
1897
+ * it always prints), or to the end of the text.
1898
+ *
1899
+ * A value may hold spaces, so the block cannot be split back into its
1900
+ * entries reliably. Every word shaped like NAME= keeps its NAME and has the
1901
+ * rest of it masked, and every other word (the tail of a value that held a
1902
+ * space or a newline) is dropped: over-masking accepted, never a value left.
1903
+ */
1904
+ const DOCKER_PRETTY_ENV_LINE_REGEX: RegExp = /^([ \t]*Env:[ \t]+)(\S.*?)(\r?)$/;
1905
+ const DOCKER_PRETTY_ENV_NAME_REGEX: RegExp = /^[A-Za-z_][A-Za-z0-9_.-]*=/;
1906
+ const DOCKER_PRETTY_CONTAINER_SPEC_REGEX: RegExp = /^ContainerSpec:[ \t]*\r?$/;
1907
+ // The lines the pretty template may start after the Env line (and the next service's ID).
1908
+ const DOCKER_PRETTY_SECTION_REGEX: RegExp =
1909
+ /^(?: (?:Dir|Init|User|Healthcheck):|(?:ID|Capabilities|SysCtls|Ulimits|Mounts|Configs|Secrets|Log Driver|Resources|Networks|Endpoint Mode|Ports):)/;
1910
+
1911
+ function maskDockerPrettyEnvEntries(entries: string): {
1912
+ text: string;
1913
+ count: number;
1914
+ } {
1915
+ const masked: Array<string> = [];
1916
+ let count: number = 0;
1917
+
1918
+ for (const word of entries.split(/\s+/)) {
1919
+ const name: RegExpExecArray | null =
1920
+ DOCKER_PRETTY_ENV_NAME_REGEX.exec(word);
1921
+
1922
+ if (!name) {
1923
+ continue;
1924
+ }
1925
+
1926
+ if (word.length > name[0].length) {
1927
+ count++;
1928
+ masked.push(`${name[0]}${RESOURCE_REDACTED_MARKER}`);
1929
+ } else {
1930
+ masked.push(word);
1931
+ }
1932
+ }
1933
+
1934
+ return { text: masked.join(" "), count };
1935
+ }
1936
+
1937
+ function maskDockerPrettyEnvLines(data: {
1938
+ resourceType: AiResourceType;
1939
+ program: string;
1940
+ text: string;
1941
+ }): ResourceOutputRedaction {
1942
+ const lines: Array<string> = data.text.split("\n");
1943
+ const out: Array<string> = [];
1944
+ let count: number = 0;
1945
+ let inContainerSpec: boolean = false;
1946
+
1947
+ for (let index: number = 0; index < lines.length; index++) {
1948
+ const line: string = lines[index] || "";
1949
+
1950
+ if (DOCKER_PRETTY_CONTAINER_SPEC_REGEX.test(line)) {
1951
+ inContainerSpec = true;
1952
+ }
1953
+
1954
+ const match: RegExpExecArray | null =
1955
+ DOCKER_PRETTY_ENV_LINE_REGEX.exec(line);
1956
+
1957
+ if (!match) {
1958
+ out.push(line);
1959
+ continue;
1960
+ }
1961
+
1962
+ const block: Array<string> = [match[2] || ""];
1963
+
1964
+ // The lines a multi-line value carried the list onto.
1965
+ while (
1966
+ inContainerSpec &&
1967
+ index + 1 < lines.length &&
1968
+ !DOCKER_PRETTY_SECTION_REGEX.test(lines[index + 1] || "")
1969
+ ) {
1970
+ index++;
1971
+ block.push(lines[index] || "");
1972
+ }
1973
+
1974
+ const masked: { text: string; count: number } = maskDockerPrettyEnvEntries(
1975
+ block.join(" "),
1976
+ );
1977
+
1978
+ count += masked.count;
1979
+ out.push(`${match[1] || ""}${masked.text}${match[3] || ""}`);
1980
+ }
1981
+
1982
+ return { text: out.join("\n"), redactionCount: count };
1983
+ }
1984
+
1985
+ /*
1986
+ * A Ceph keyring's secret: `key = AQD...==` (ceph auth get, keyring files),
1987
+ * `key: AQD...` (ceph auth ls) and `"key": "AQD..."` (-f json). The generic
1988
+ * rules deliberately leave a bare `key` alone (taints and tolerations have
1989
+ * one), but for ceph it is always a cephx secret.
1990
+ */
1991
+ const CEPH_KEY_LINE_REGEX: RegExp = /^([ \t]*key[ \t]*[:=][ \t]*)(\S.*)$/gm;
1992
+ const CEPH_KEY_JSON_REGEX: RegExp = /("key"\s*:\s*")((?:[^"\\]|\\.)*)(")/g;
1993
+
1994
+ function maskCephKeys(data: {
1995
+ resourceType: AiResourceType;
1996
+ program: string;
1997
+ text: string;
1998
+ }): ResourceOutputRedaction {
1999
+ let count: number = 0;
2000
+
2001
+ const text: string = data.text
2002
+ .replace(
2003
+ CEPH_KEY_LINE_REGEX,
2004
+ (whole: string, prefix: string, value: string): string => {
2005
+ if (value.trim() === RESOURCE_REDACTED_MARKER) {
2006
+ return whole;
2007
+ }
2008
+
2009
+ count++;
2010
+ return `${prefix}${RESOURCE_REDACTED_MARKER}`;
2011
+ },
2012
+ )
2013
+ .replace(
2014
+ CEPH_KEY_JSON_REGEX,
2015
+ (
2016
+ whole: string,
2017
+ prefix: string,
2018
+ value: string,
2019
+ suffix: string,
2020
+ ): string => {
2021
+ if (!value || value === RESOURCE_REDACTED_MARKER) {
2022
+ return whole;
2023
+ }
2024
+
2025
+ count++;
2026
+ return `${prefix}${RESOURCE_REDACTED_MARKER}${suffix}`;
2027
+ },
2028
+ );
2029
+
2030
+ return { text, redactionCount: count };
2031
+ }
2032
+
2033
+ /*
2034
+ * govc (VMware vCenter). A VM's extraConfig holds guestinfo.* keys —
2035
+ * cloud-init user data and metadata, vendor data, OVF environments — that
2036
+ * routinely carry passwords and SSH keys under names the generic rules
2037
+ * cannot recognise. The govc policy refuses every command that prints
2038
+ * extraConfig (vm.info -e and -json, object.collect of config); this is the
2039
+ * second line. The value of every option whose key is a guestinfo.* key (or
2040
+ * names user data, vendor data or an OVF environment, or looks
2041
+ * credential-like) is masked in each shape govc prints one: -json
2042
+ * OptionValue objects (key before or after value, any key case, the value a
2043
+ * string, a number or a {"_typeName", "_value"} object), the text listing
2044
+ * (`guestinfo.userdata: ...`), a JSON map entry (`"guestinfo.x": "..."`)
2045
+ * and -dump's Go syntax (`Key: "...", Value: "..."`). A vSphere session id
2046
+ * or ticket in -json output (a login event's sessionId, a clone or console
2047
+ * ticket) authenticates as its user, and is masked too.
2048
+ */
2049
+ const GOVC_JSON_STRING: string = '"(?:[^"\\\\]|\\\\.)*"';
2050
+ const GOVC_JSON_NUMBER: string = "-?[0-9][0-9.eE+-]*";
2051
+ const GOVC_JSON_SCALAR: string = `(?:${GOVC_JSON_STRING}|${GOVC_JSON_NUMBER})`;
2052
+ const GOVC_TYPED_VALUE: string = `\\{\\s*(?:"_typeName"\\s*:\\s*"[^"]*"\\s*,\\s*)?"_value"\\s*:\\s*${GOVC_JSON_SCALAR}(?:\\s*,\\s*"_typeName"\\s*:\\s*"[^"]*")?\\s*\\}`;
2053
+ const GOVC_OPTION_VALUE: string = `(?:${GOVC_JSON_SCALAR}|${GOVC_TYPED_VALUE})`;
2054
+
2055
+ const GOVC_OPTION_KEY_FIRST_REGEX: RegExp = new RegExp(
2056
+ `("key"\\s*:\\s*")((?:[^"\\\\]|\\\\.)*)("\\s*,\\s*"value"\\s*:\\s*)(${GOVC_OPTION_VALUE})`,
2057
+ "gi",
2058
+ );
2059
+ const GOVC_OPTION_VALUE_FIRST_REGEX: RegExp = new RegExp(
2060
+ `("value"\\s*:\\s*)(${GOVC_OPTION_VALUE})(\\s*,\\s*"key"\\s*:\\s*")((?:[^"\\\\]|\\\\.)*)(")`,
2061
+ "gi",
2062
+ );
2063
+ const GOVC_OPTION_DUMP_REGEX: RegExp =
2064
+ /(\bKey:\s*")((?:[^"\\]|\\.)*)("\s*,\s*Value:\s*)("(?:[^"\\]|\\.)*"|[^,\n}]*)/g;
2065
+ const GOVC_GUESTINFO_LINE_REGEX: RegExp =
2066
+ /^([ \t]*guestinfo\.[^\s:=]*[ \t]*[:=][ \t]*)(\S.*)$/gim;
2067
+ const GOVC_SECRET_JSON_PAIR_REGEX: RegExp =
2068
+ /("(?:sessionId|ticket|cloneTicket|guestinfo\.[^"\\\n]*)"\s*:\s*")((?:[^"\\]|\\.)*)(")/gi;
2069
+
2070
+ // An option key whose value is (or may hold) credential material.
2071
+ function isSensitiveGovcOptionKey(key: string): boolean {
2072
+ const folded: string = key.trim().toLowerCase();
2073
+
2074
+ return (
2075
+ folded.startsWith("guestinfo.") ||
2076
+ folded.includes("userdata") ||
2077
+ folded.includes("vendordata") ||
2078
+ folded.includes("ovfenv") ||
2079
+ looksLikeCredentialName(folded)
2080
+ );
2081
+ }
2082
+
2083
+ // A value this hook (or an earlier pass) has nothing left to mask in.
2084
+ function isMaskedOrEmptyGovcValue(value: string): boolean {
2085
+ const body: string = value.trim();
2086
+
2087
+ return (
2088
+ body === "" ||
2089
+ body === '""' ||
2090
+ body === RESOURCE_REDACTED_MARKER ||
2091
+ body === `"${RESOURCE_REDACTED_MARKER}"`
2092
+ );
2093
+ }
2094
+
2095
+ function maskGovcSecrets(data: {
2096
+ resourceType: AiResourceType;
2097
+ program: string;
2098
+ text: string;
2099
+ }): ResourceOutputRedaction {
2100
+ let count: number = 0;
2101
+ const maskedJson: string = `"${RESOURCE_REDACTED_MARKER}"`;
2102
+
2103
+ const text: string = data.text
2104
+ .replace(
2105
+ GOVC_OPTION_KEY_FIRST_REGEX,
2106
+ (
2107
+ whole: string,
2108
+ keyPrefix: string,
2109
+ key: string,
2110
+ separator: string,
2111
+ value: string,
2112
+ ): string => {
2113
+ if (!isSensitiveGovcOptionKey(key) || isMaskedOrEmptyGovcValue(value)) {
2114
+ return whole;
2115
+ }
2116
+
2117
+ count++;
2118
+ return `${keyPrefix}${key}${separator}${maskedJson}`;
2119
+ },
2120
+ )
2121
+ .replace(
2122
+ GOVC_OPTION_VALUE_FIRST_REGEX,
2123
+ (
2124
+ whole: string,
2125
+ valuePrefix: string,
2126
+ value: string,
2127
+ separator: string,
2128
+ key: string,
2129
+ closingQuote: string,
2130
+ ): string => {
2131
+ if (!isSensitiveGovcOptionKey(key) || isMaskedOrEmptyGovcValue(value)) {
2132
+ return whole;
2133
+ }
2134
+
2135
+ count++;
2136
+ return `${valuePrefix}${maskedJson}${separator}${key}${closingQuote}`;
2137
+ },
2138
+ )
2139
+ .replace(
2140
+ GOVC_OPTION_DUMP_REGEX,
2141
+ (
2142
+ whole: string,
2143
+ keyPrefix: string,
2144
+ key: string,
2145
+ separator: string,
2146
+ value: string,
2147
+ ): string => {
2148
+ if (!isSensitiveGovcOptionKey(key) || isMaskedOrEmptyGovcValue(value)) {
2149
+ return whole;
2150
+ }
2151
+
2152
+ count++;
2153
+ return `${keyPrefix}${key}${separator}${maskedJson}`;
2154
+ },
2155
+ )
2156
+ .replace(
2157
+ GOVC_GUESTINFO_LINE_REGEX,
2158
+ (whole: string, prefix: string, value: string): string => {
2159
+ if (isMaskedOrEmptyGovcValue(value)) {
2160
+ return whole;
2161
+ }
2162
+
2163
+ count++;
2164
+ return `${prefix}${RESOURCE_REDACTED_MARKER}`;
2165
+ },
2166
+ )
2167
+ .replace(
2168
+ GOVC_SECRET_JSON_PAIR_REGEX,
2169
+ (
2170
+ whole: string,
2171
+ prefix: string,
2172
+ value: string,
2173
+ suffix: string,
2174
+ ): string => {
2175
+ if (isMaskedOrEmptyGovcValue(value)) {
2176
+ return whole;
2177
+ }
2178
+
2179
+ count++;
2180
+ return `${prefix}${RESOURCE_REDACTED_MARKER}${suffix}`;
2181
+ },
2182
+ );
2183
+
2184
+ return { text, redactionCount: count };
2185
+ }
2186
+
2187
+ /*
2188
+ * The rest of a Ceph secret's footprint, after maskCephKeys:
2189
+ * - a bare cephx secret wherever it appears (`ceph auth print-key` prints
2190
+ * one alone; an audit entry or an escaped JSON string can hold one). A
2191
+ * cephx secret is the base64 of a 28-byte key record whose type is 1, so
2192
+ * it starts "AQ" and is 40 characters long with "==" padding; the
2193
+ * pattern is looser than that on purpose;
2194
+ * - the value of a `"val"` or `"value"` field, plain or escaped inside
2195
+ * another JSON string: the audit channel (`ceph log last ... audit`)
2196
+ * records every `config-key set` and `config set` a client ran, value
2197
+ * included, and those values are where dashboard, RGW and cephadm
2198
+ * credentials live.
2199
+ */
2200
+ const CEPHX_SECRET_REGEX: RegExp =
2201
+ /(^|[^A-Za-z0-9+/])AQ[A-Za-z0-9+/]{30,}={0,2}/g;
2202
+ const CEPH_VALUE_JSON_REGEX: RegExp =
2203
+ /("(?:val|value)"\s*:\s*")((?:[^"\\]|\\.)*)(")/g;
2204
+ const CEPH_VALUE_ESCAPED_JSON_REGEX: RegExp =
2205
+ /(\\"(?:val|value)\\"\s*:\s*\\")((?:[^"\\]|\\\\\\["\\]|\\\\[^"\\])*)(\\")/g;
2206
+
2207
+ function maskCephSecretValues(data: {
2208
+ resourceType: AiResourceType;
2209
+ program: string;
2210
+ text: string;
2211
+ }): ResourceOutputRedaction {
2212
+ let count: number = 0;
2213
+
2214
+ const maskValue: (
2215
+ whole: string,
2216
+ prefix: string,
2217
+ value: string,
2218
+ suffix: string,
2219
+ ) => string = (
2220
+ whole: string,
2221
+ prefix: string,
2222
+ value: string,
2223
+ suffix: string,
2224
+ ): string => {
2225
+ if (!value || value === RESOURCE_REDACTED_MARKER) {
2226
+ return whole;
2227
+ }
2228
+
2229
+ count++;
2230
+ return `${prefix}${RESOURCE_REDACTED_MARKER}${suffix}`;
2231
+ };
2232
+
2233
+ const text: string = data.text
2234
+ .replace(CEPH_VALUE_JSON_REGEX, maskValue)
2235
+ .replace(CEPH_VALUE_ESCAPED_JSON_REGEX, maskValue)
2236
+ .replace(CEPHX_SECRET_REGEX, (_whole: string, before: string): string => {
2237
+ count++;
2238
+ return `${before}${RESOURCE_REDACTED_MARKER}`;
2239
+ });
2240
+
2241
+ return { text, redactionCount: count };
2242
+ }
2243
+
2244
+ /*
2245
+ * pvesh (Proxmox VE). A guest's config and pending changes carry
2246
+ * cloud-init's `cipassword`, and the cluster storage list can carry a
2247
+ * `password`, an RBD `keyring` or a PBS `encryption-key`. Masked in every
2248
+ * shape the agent prints the API's answer in: JSON pairs (plain, or escaped
2249
+ * inside another JSON string), `key: value` and `key value` lines, the rows
2250
+ * of pvesh's text tables (`│ cipassword │ ... │`), and /pending entries —
2251
+ * {"key": "cipassword", "value": ..., "pending": ...}, members in any order
2252
+ * — where the secret's NAME is itself a value, so the generic rules never
2253
+ * read it as a key. In the table and pending shapes any credential-looking
2254
+ * name (isCredentialKey) counts too; the generic rules cover it elsewhere.
2255
+ */
2256
+ const PROXMOX_SECRET_KEYS: ReadonlyArray<string> = [
2257
+ "cipassword",
2258
+ "password",
2259
+ "keyring",
2260
+ "encryption-key",
2261
+ ];
2262
+ const PROXMOX_SECRET_KEY_SOURCE: string = PROXMOX_SECRET_KEYS.join("|");
2263
+ const PROXMOX_SECRET_JSON_REGEX: RegExp = new RegExp(
2264
+ `("(?:${PROXMOX_SECRET_KEY_SOURCE})"\\s*:\\s*")((?:[^"\\\\]|\\\\.)*)(")`,
2265
+ "gi",
2266
+ );
2267
+ const PROXMOX_SECRET_ESCAPED_JSON_REGEX: RegExp = new RegExp(
2268
+ `(\\\\"(?:${PROXMOX_SECRET_KEY_SOURCE})\\\\"\\s*:\\s*\\\\")((?:[^"\\\\]|\\\\[^"])*)(\\\\")`,
2269
+ "gi",
2270
+ );
2271
+ const PROXMOX_SECRET_LINE_REGEX: RegExp = new RegExp(
2272
+ `^([ \\t]*(?:-[ \\t]+)?(?:${PROXMOX_SECRET_KEY_SOURCE})(?:[ \\t]*[:=][ \\t]*|[ \\t]+))(\\S.*)$`,
2273
+ "gim",
2274
+ );
2275
+ const PROXMOX_FLAT_JSON_OBJECT_REGEX: RegExp = /\{[^{}]*\}/g;
2276
+ const PROXMOX_PENDING_KEY_REGEX: RegExp = /"key"\s*:\s*"((?:[^"\\]|\\.)*)"/;
2277
+ const PROXMOX_PENDING_VALUE_REGEX: RegExp =
2278
+ /("(?:value|pending)"\s*:\s*)("(?:[^"\\]|\\.)*"|-?[0-9][0-9.eE+-]*)/g;
2279
+ const PROXMOX_TABLE_ROW_REGEX: RegExp = /^([ \t]*)([│|])(.*)$/gm;
2280
+ const PROXMOX_CELL_CONTENT_REGEX: RegExp = /\S(?:.*\S)?/;
2281
+
2282
+ function isProxmoxSecretName(name: string): boolean {
2283
+ const folded: string = name.trim().toLowerCase();
2284
+
2285
+ return PROXMOX_SECRET_KEYS.includes(folded) || isCredentialKey(folded);
2286
+ }
2287
+
2288
+ // A value an earlier pass already masked, or one with nothing in it.
2289
+ function isMaskedOrEmptyProxmoxValue(value: string): boolean {
2290
+ const body: string = unquote(value.trim().replace(/,$/, "").trim());
2291
+
2292
+ return body === "" || body.startsWith(REDACTED_PREFIX);
2293
+ }
2294
+
2295
+ function maskProxmoxSecrets(data: {
2296
+ resourceType: AiResourceType;
2297
+ program: string;
2298
+ text: string;
2299
+ }): ResourceOutputRedaction {
2300
+ let count: number = 0;
2301
+
2302
+ const maskPair: (
2303
+ whole: string,
2304
+ prefix: string,
2305
+ value: string,
2306
+ suffix: string,
2307
+ ) => string = (
2308
+ whole: string,
2309
+ prefix: string,
2310
+ value: string,
2311
+ suffix: string,
2312
+ ): string => {
2313
+ if (isMaskedOrEmptyProxmoxValue(value)) {
2314
+ return whole;
2315
+ }
2316
+
2317
+ count++;
2318
+ return `${prefix}${RESOURCE_REDACTED_MARKER}${suffix}`;
2319
+ };
2320
+
2321
+ const text: string = data.text
2322
+ .replace(PROXMOX_SECRET_JSON_REGEX, maskPair)
2323
+ .replace(PROXMOX_SECRET_ESCAPED_JSON_REGEX, maskPair)
2324
+ .replace(PROXMOX_FLAT_JSON_OBJECT_REGEX, (entry: string): string => {
2325
+ const key: RegExpExecArray | null = PROXMOX_PENDING_KEY_REGEX.exec(entry);
2326
+
2327
+ if (!key || !isProxmoxSecretName(key[1] || "")) {
2328
+ return entry;
2329
+ }
2330
+
2331
+ return entry.replace(
2332
+ PROXMOX_PENDING_VALUE_REGEX,
2333
+ (whole: string, prefix: string, value: string): string => {
2334
+ if (isMaskedOrEmptyProxmoxValue(value)) {
2335
+ return whole;
2336
+ }
2337
+
2338
+ count++;
2339
+ return `${prefix}"${RESOURCE_REDACTED_MARKER}"`;
2340
+ },
2341
+ );
2342
+ })
2343
+ .replace(
2344
+ PROXMOX_SECRET_LINE_REGEX,
2345
+ (whole: string, prefix: string, value: string): string => {
2346
+ if (isMaskedOrEmptyProxmoxValue(value)) {
2347
+ return whole;
2348
+ }
2349
+
2350
+ count++;
2351
+ return `${prefix}${RESOURCE_REDACTED_MARKER}`;
2352
+ },
2353
+ )
2354
+ .replace(
2355
+ PROXMOX_TABLE_ROW_REGEX,
2356
+ (
2357
+ whole: string,
2358
+ indent: string,
2359
+ separator: string,
2360
+ rest: string,
2361
+ ): string => {
2362
+ const cells: Array<string> = rest.split(separator);
2363
+
2364
+ if (cells.length < 2 || !isProxmoxSecretName(cells[0] || "")) {
2365
+ return whole;
2366
+ }
2367
+
2368
+ const masked: Array<string> = cells.map(
2369
+ (cell: string, index: number): string => {
2370
+ if (index === 0 || isMaskedOrEmptyProxmoxValue(cell)) {
2371
+ return cell;
2372
+ }
2373
+
2374
+ count++;
2375
+ return cell.replace(
2376
+ PROXMOX_CELL_CONTENT_REGEX,
2377
+ RESOURCE_REDACTED_MARKER,
2378
+ );
2379
+ },
2380
+ );
2381
+
2382
+ return `${indent}${separator}${masked.join(separator)}`;
2383
+ },
2384
+ );
2385
+
2386
+ return { text, redactionCount: count };
2387
+ }
2388
+
2389
+ /*
2390
+ * Credentials on the command lines a process listing prints (ps, top -c,
2391
+ * the cgroup tree of systemctl status, journalctl):
2392
+ * the value after a credential-named flag (`--password=x`, `--token x`,
2393
+ * `-Dapp.db.password=x`), a credential-named assignment (`PASSWORD=x`,
2394
+ * `DB_TOKEN=x`) and the password half of `-u user:password` (curl, wget).
2395
+ * ps and top join a process's argv with single spaces and drop its quotes,
2396
+ * so a value is the run of characters up to the next whitespace. Names
2397
+ * are judged by the generic rules' own isCredentialKey, so both layers
2398
+ * agree on what a credential is called.
2399
+ */
2400
+ const PROCESS_USER_PASSWORD_FLAGS: ReadonlyArray<string> = [
2401
+ "-u",
2402
+ "--user",
2403
+ "-U",
2404
+ "--proxy-user",
2405
+ ];
2406
+
2407
+ const PROCESS_NUMERIC_REGEX: RegExp = /^[0-9]+$/;
2408
+
2409
+ // One process line with its credential values masked, and how many.
2410
+ function maskProcessLine(line: string): { text: string; count: number } {
2411
+ // Words at even indexes, the whitespace between them at odd ones.
2412
+ const parts: Array<string> = line.split(/(\s+)/);
2413
+ let count: number = 0;
2414
+ let maskNextWord: boolean = false;
2415
+ let userPasswordNext: boolean = false;
2416
+
2417
+ for (let i: number = 0; i < parts.length; i += 2) {
2418
+ const word: string = parts[i] || "";
2419
+
2420
+ if (!word) {
2421
+ continue;
2422
+ }
2423
+
2424
+ if (maskNextWord) {
2425
+ maskNextWord = false;
2426
+
2427
+ if (!word.startsWith("-") && !isExemptValue(word)) {
2428
+ parts[i] = RESOURCE_REDACTED_MARKER;
2429
+ count++;
2430
+ continue;
2431
+ }
2432
+ }
2433
+
2434
+ if (userPasswordNext) {
2435
+ userPasswordNext = false;
2436
+ const colon: number = word.indexOf(":");
2437
+ const password: string = colon > 0 ? word.slice(colon + 1) : "";
2438
+
2439
+ if (
2440
+ password &&
2441
+ !word.startsWith("-") &&
2442
+ !PROCESS_NUMERIC_REGEX.test(password) &&
2443
+ !isExemptValue(password)
2444
+ ) {
2445
+ parts[i] = `${word.slice(0, colon + 1)}${RESOURCE_REDACTED_MARKER}`;
2446
+ count++;
2447
+ continue;
2448
+ }
2449
+ }
2450
+
2451
+ const equals: number = word.indexOf("=");
2452
+
2453
+ if (equals > 0) {
2454
+ const value: string = word.slice(equals + 1);
2455
+
2456
+ if (
2457
+ value &&
2458
+ !isExemptValue(value) &&
2459
+ isCredentialKey(word.slice(0, equals))
2460
+ ) {
2461
+ parts[i] = `${word.slice(0, equals + 1)}${RESOURCE_REDACTED_MARKER}`;
2462
+ count++;
2463
+ }
2464
+ continue;
2465
+ }
2466
+
2467
+ if (!word.startsWith("-") || word.length < 2) {
2468
+ continue;
2469
+ }
2470
+
2471
+ if (PROCESS_USER_PASSWORD_FLAGS.includes(word)) {
2472
+ userPasswordNext = true;
2473
+ continue;
2474
+ }
2475
+
2476
+ if (isCredentialKey(word)) {
2477
+ maskNextWord = true;
2478
+ }
2479
+ }
2480
+
2481
+ return { text: parts.join(""), count };
2482
+ }
2483
+
2484
+ function maskProcessCredentialArguments(data: {
2485
+ resourceType: AiResourceType;
2486
+ program: string;
2487
+ text: string;
2488
+ }): ResourceOutputRedaction {
2489
+ let count: number = 0;
2490
+
2491
+ const lines: Array<string> = data.text
2492
+ .split("\n")
2493
+ .map((line: string): string => {
2494
+ const masked: { text: string; count: number } = maskProcessLine(line);
2495
+ count += masked.count;
2496
+ return masked.text;
2497
+ });
2498
+
2499
+ return { text: lines.join("\n"), redactionCount: count };
2500
+ }
2501
+
2502
+ /*
2503
+ * journalctl prints the command lines of the processes it logged: in a
2504
+ * message, and with -o json in each record's _CMDLINE field. A JSON record
2505
+ * (one per line) has each of its string values masked as a process line —
2506
+ * decoded first and re-encoded after, so the record stays valid JSON; any
2507
+ * other line is masked as a process line itself.
2508
+ */
2509
+ function maskJournalCredentialArguments(data: {
2510
+ resourceType: AiResourceType;
2511
+ program: string;
2512
+ text: string;
2513
+ }): ResourceOutputRedaction {
2514
+ let count: number = 0;
2515
+
2516
+ const lines: Array<string> = data.text
2517
+ .split("\n")
2518
+ .map((line: string): string => {
2519
+ const trimmed: string = line.trim();
2520
+
2521
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
2522
+ let record: unknown = null;
2523
+
2524
+ try {
2525
+ record = JSON.parse(trimmed);
2526
+ } catch {
2527
+ record = null;
2528
+ }
2529
+
2530
+ if (record && typeof record === "object" && !Array.isArray(record)) {
2531
+ const fields: Record<string, unknown> = record as Record<
2532
+ string,
2533
+ unknown
2534
+ >;
2535
+ let changed: boolean = false;
2536
+
2537
+ for (const key of Object.keys(fields)) {
2538
+ const value: unknown = fields[key];
2539
+
2540
+ if (typeof value !== "string") {
2541
+ continue;
2542
+ }
2543
+
2544
+ const masked: { text: string; count: number } =
2545
+ maskProcessLine(value);
2546
+
2547
+ if (masked.count > 0) {
2548
+ fields[key] = masked.text;
2549
+ count += masked.count;
2550
+ changed = true;
2551
+ }
2552
+ }
2553
+
2554
+ if (!changed) {
2555
+ return line;
2556
+ }
2557
+
2558
+ /*
2559
+ * Spliced in by position, never through String.replace: a
2560
+ * replacement string's "$&" (a MESSAGE may hold one) would paste
2561
+ * the original, unmasked record back in.
2562
+ */
2563
+ const start: number = line.indexOf(trimmed);
2564
+
2565
+ return `${line.slice(0, start)}${JSON.stringify(fields)}${line.slice(
2566
+ start + trimmed.length,
2567
+ )}`;
2568
+ }
2569
+ }
2570
+
2571
+ const masked: { text: string; count: number } = maskProcessLine(line);
2572
+ count += masked.count;
2573
+ return masked.text;
2574
+ });
2575
+
2576
+ return { text: lines.join("\n"), redactionCount: count };
2577
+ }
2578
+
2579
+ /*
2580
+ * `ceph status` (and `ceph -s`) in its plain format calls its usage section
2581
+ * "data:" — pools, objects, usage and PG states — and the generic rules,
2582
+ * written for kubectl, read a "data:" block as a Kubernetes Secret's and
2583
+ * mask every line of it, which leaves the model blind to the one summary it
2584
+ * asks for first. For exactly that shape — a " data:" header after the
2585
+ * " cluster:" / " id: FSID" / " services:" headers, holding only
2586
+ * ceph's own usage lines and their continuation lines — the header is
2587
+ * renamed to a placeholder the generic rules read as an ordinary key while
2588
+ * they run, and written back afterwards. Every line under it still gets the
2589
+ * generic per-line rules. Any other shape (and output that already holds
2590
+ * the placeholder) is left to the generic rules whole: over-masking is
2591
+ * accepted, under-masking is not.
2592
+ */
2593
+ const CEPH_STATUS_DATA_PLACEHOLDER: string = "ceph-status-usage-section";
2594
+ const CEPH_STATUS_DATA_HEADER: string = " data:";
2595
+ const CEPH_STATUS_DATA_KEYS: ReadonlyArray<string> = [
2596
+ "volumes",
2597
+ "pools",
2598
+ "objects",
2599
+ "usage",
2600
+ "pgs",
2601
+ ];
2602
+ const CEPH_STATUS_FSID_LINE_REGEX: RegExp =
2603
+ /^ {4}id: +[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
2604
+ const CEPH_STATUS_DATA_ENTRY_REGEX: RegExp = /^ {4}([a-z]+):(?: |$)/;
2605
+ const CEPH_STATUS_PLACEHOLDER_LINE_REGEX: RegExp = new RegExp(
2606
+ `^( {2})${CEPH_STATUS_DATA_PLACEHOLDER}:`,
2607
+ );
2608
+
2609
+ export interface ShieldedOutput {
2610
+ // The output as the generic rules should read it.
2611
+ text: string;
2612
+ // The generic rules' answer with the shielded header written back.
2613
+ restore: (redacted: string) => string;
2614
+ }
2615
+
2616
+ export function shieldCephStatusDataSection(
2617
+ text: string,
2618
+ ): ShieldedOutput | null {
2619
+ if (typeof text !== "string" || text.includes(CEPH_STATUS_DATA_PLACEHOLDER)) {
2620
+ return null;
2621
+ }
2622
+
2623
+ const lines: Array<string> = text.split("\n");
2624
+ const bare: Array<string> = lines.map((line: string): string => {
2625
+ return line.replace(/\r$/, "");
2626
+ });
2627
+ const clusterIndex: number = bare.indexOf(" cluster:");
2628
+
2629
+ if (
2630
+ clusterIndex < 0 ||
2631
+ !CEPH_STATUS_FSID_LINE_REGEX.test(bare[clusterIndex + 1] || "")
2632
+ ) {
2633
+ return null;
2634
+ }
2635
+
2636
+ const servicesIndex: number = bare.indexOf(" services:", clusterIndex);
2637
+ const dataIndex: number =
2638
+ servicesIndex < 0
2639
+ ? -1
2640
+ : bare.indexOf(CEPH_STATUS_DATA_HEADER, servicesIndex);
2641
+
2642
+ if (dataIndex < 0) {
2643
+ return null;
2644
+ }
2645
+
2646
+ let entries: number = 0;
2647
+
2648
+ for (let index: number = dataIndex + 1; index < bare.length; index++) {
2649
+ const line: string = bare[index] as string;
2650
+
2651
+ if (line.trim() === "") {
2652
+ break;
2653
+ }
2654
+
2655
+ const indent: number = line.length - line.trimStart().length;
2656
+
2657
+ if (indent <= 2) {
2658
+ break;
2659
+ }
2660
+
2661
+ if (indent === 4) {
2662
+ const entry: RegExpExecArray | null =
2663
+ CEPH_STATUS_DATA_ENTRY_REGEX.exec(line);
2664
+
2665
+ if (!entry || !CEPH_STATUS_DATA_KEYS.includes(entry[1] || "")) {
2666
+ return null;
2667
+ }
2668
+
2669
+ entries++;
2670
+ continue;
2671
+ }
2672
+
2673
+ // Deeper: the continuation of the entry above (" 12 active+clean").
2674
+ if (indent > 4 && entries > 0) {
2675
+ continue;
2676
+ }
2677
+
2678
+ return null;
2679
+ }
2680
+
2681
+ if (entries === 0) {
2682
+ return null;
2683
+ }
2684
+
2685
+ const shielded: Array<string> = lines.slice();
2686
+ shielded[dataIndex] = (lines[dataIndex] as string).replace(
2687
+ "data:",
2688
+ `${CEPH_STATUS_DATA_PLACEHOLDER}:`,
2689
+ );
2690
+
2691
+ return {
2692
+ text: shielded.join("\n"),
2693
+ restore: (redacted: string): string => {
2694
+ return redacted
2695
+ .split("\n")
2696
+ .map((line: string): string => {
2697
+ return line.replace(CEPH_STATUS_PLACEHOLDER_LINE_REGEX, "$1data:");
2698
+ })
2699
+ .join("\n");
2700
+ },
2701
+ };
2702
+ }
2703
+
2704
+ /*
2705
+ * Output a program prints that the generic rules misread, shielded while
2706
+ * they run (see shieldCephStatusDataSection); null for everything else.
2707
+ */
2708
+ function shieldFromGenericRules(
2709
+ program: string,
2710
+ text: string,
2711
+ ): ShieldedOutput | null {
2712
+ try {
2713
+ return program === "ceph" ? shieldCephStatusDataSection(text) : null;
2714
+ } catch {
2715
+ return null;
2716
+ }
2717
+ }
2718
+
2719
+ /*
2720
+ * The per-program hooks, run on the raw output before the generic rules.
2721
+ * Keyed by argv[0]; a program with no entry gets the generic rules only.
2722
+ * Each tool's kit extends its program's list (pvesh `cipassword`, govc
2723
+ * credentials, db query literals, ...).
2724
+ */
2725
+ export const RESOURCE_OUTPUT_REDACTION_HOOKS: Readonly<
2726
+ Record<string, ReadonlyArray<ResourceOutputRedactionHook>>
2727
+ > = {
2728
+ ps: [maskProcessCredentialArguments],
2729
+ top: [maskProcessCredentialArguments],
2730
+ // systemctl status prints every process of a unit's cgroup, argv and all.
2731
+ systemctl: [maskProcessCredentialArguments],
2732
+ journalctl: [maskJournalCredentialArguments],
2733
+ docker: [maskDockerEnvValues, maskDockerPrettyEnvLines],
2734
+ ceph: [maskCephKeys, maskCephSecretValues],
2735
+ pvesh: [maskProxmoxSecrets],
2736
+ govc: [maskGovcSecrets],
2737
+ db: [redactDatabaseOutput],
2738
+ };
2739
+
2740
+ // The hooks for one program; none for a program the table does not list.
2741
+ export function getResourceOutputRedactionHooks(
2742
+ program: string,
2743
+ ): ReadonlyArray<ResourceOutputRedactionHook> {
2744
+ if (
2745
+ typeof program !== "string" ||
2746
+ !Object.prototype.hasOwnProperty.call(
2747
+ RESOURCE_OUTPUT_REDACTION_HOOKS,
2748
+ program,
2749
+ )
2750
+ ) {
2751
+ return [];
2752
+ }
2753
+
2754
+ return RESOURCE_OUTPUT_REDACTION_HOOKS[program] || [];
2755
+ }
2756
+
2757
+ /*
2758
+ * Redact one command's output: the program's hooks, then the generic rules.
2759
+ * Total: anything that is not text comes back as "", and a hook that throws
2760
+ * is skipped (the generic rules still run over what it was given).
2761
+ */
2762
+ export function redactResourceCommandOutputWithCount(data: {
2763
+ resourceType: AiResourceType;
2764
+ program: string;
2765
+ text: string;
2766
+ }): ResourceOutputRedaction {
2767
+ if (!data || typeof data.text !== "string" || !data.text) {
2768
+ return { text: "", redactionCount: 0 };
2769
+ }
2770
+
2771
+ let text: string = data.text;
2772
+ let count: number = 0;
2773
+
2774
+ for (const hook of getResourceOutputRedactionHooks(data.program)) {
2775
+ try {
2776
+ const hooked: ResourceOutputRedaction = hook({
2777
+ resourceType: data.resourceType,
2778
+ program: data.program,
2779
+ text,
2780
+ });
2781
+
2782
+ if (hooked && typeof hooked.text === "string") {
2783
+ text = hooked.text;
2784
+ count +=
2785
+ typeof hooked.redactionCount === "number" ? hooked.redactionCount : 0;
2786
+ }
2787
+ } catch {
2788
+ // A broken hook must never leak the output unredacted by the generic rules.
2789
+ }
2790
+ }
2791
+
2792
+ const shield: ShieldedOutput | null = shieldFromGenericRules(
2793
+ data.program,
2794
+ text,
2795
+ );
2796
+ const generic: ResourceOutputRedaction = GenericOutputRedactor.redact(
2797
+ shield ? shield.text : text,
2798
+ );
2799
+
2800
+ return {
2801
+ text: shield ? shield.restore(generic.text) : generic.text,
2802
+ redactionCount: count + generic.redactionCount,
2803
+ };
2804
+ }
2805
+
2806
+ export function redactResourceCommandOutput(data: {
2807
+ resourceType: AiResourceType;
2808
+ program: string;
2809
+ text: string;
2810
+ }): string {
2811
+ return redactResourceCommandOutputWithCount(data).text;
2812
+ }