@oneuptime/common 12.0.2 → 12.0.4

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 (384) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +14 -0
  2. package/Models/DatabaseModels/CodeRepository.ts +14 -6
  3. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  4. package/Models/DatabaseModels/DockerHost.ts +14 -0
  5. package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
  6. package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
  7. package/Models/DatabaseModels/Index.ts +2 -0
  8. package/Models/DatabaseModels/IoTFleet.ts +14 -0
  9. package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
  10. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  11. package/Models/DatabaseModels/NetworkDevice.ts +14 -0
  12. package/Models/DatabaseModels/NetworkInterface.ts +18 -0
  13. package/Models/DatabaseModels/Project.ts +16 -2
  14. package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
  15. package/Models/DatabaseModels/Service.ts +30 -0
  16. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  17. package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
  18. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  19. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  20. package/Models/DatabaseModels/User.ts +30 -0
  21. package/Server/API/AIAgentDataAPI.ts +31 -0
  22. package/Server/API/BaseAPI.ts +13 -2
  23. package/Server/API/DashboardAPI.ts +458 -12
  24. package/Server/API/GitHubAPI.ts +119 -283
  25. package/Server/API/UserAPI.ts +263 -1
  26. package/Server/EnvironmentConfig.ts +38 -4
  27. package/Server/Infrastructure/GlobalCache.ts +152 -0
  28. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  36. package/Server/Services/AlertStateTimelineService.ts +20 -0
  37. package/Server/Services/AnalyticsDatabaseService.ts +72 -1
  38. package/Server/Services/CephClusterService.ts +26 -86
  39. package/Server/Services/CloudResourceService.ts +23 -83
  40. package/Server/Services/CodeRepositoryService.ts +105 -2
  41. package/Server/Services/DatabaseService.ts +296 -2
  42. package/Server/Services/DockerHostService.ts +19 -79
  43. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  44. package/Server/Services/HostService.ts +46 -99
  45. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  46. package/Server/Services/Index.ts +2 -0
  47. package/Server/Services/IoTFleetService.ts +19 -79
  48. package/Server/Services/KubernetesClusterService.ts +17 -78
  49. package/Server/Services/LabelService.ts +57 -10
  50. package/Server/Services/MetricTypeService.ts +84 -0
  51. package/Server/Services/MonitorService.ts +89 -14
  52. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  53. package/Server/Services/MonitorTemplateService.ts +65 -0
  54. package/Server/Services/OpenTelemetryIngestService.ts +230 -26
  55. package/Server/Services/PodmanHostService.ts +19 -79
  56. package/Server/Services/ProxmoxClusterService.ts +23 -83
  57. package/Server/Services/RumApplicationService.ts +19 -80
  58. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  59. package/Server/Services/ServerlessFunctionService.ts +24 -85
  60. package/Server/Services/ServiceService.ts +299 -73
  61. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  62. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  63. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  64. package/Server/Services/TelemetryEntityService.ts +1 -0
  65. package/Server/Services/UserService.ts +184 -0
  66. package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
  67. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
  68. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
  69. package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
  70. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  71. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  72. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  73. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  74. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  75. package/Server/Utils/PasswordHash.ts +306 -0
  76. package/Server/Utils/SingleFlight.ts +83 -0
  77. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  78. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  79. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  80. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  81. package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +333 -0
  82. package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
  83. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  84. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  85. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  86. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  87. package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
  88. package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
  89. package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
  90. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  91. package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
  92. package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
  93. package/Tests/Server/API/Helpers.ts +6 -1
  94. package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
  95. package/Tests/Server/Infrastructure/GlobalCache.test.ts +354 -0
  96. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  97. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
  98. package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
  99. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
  100. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
  101. package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
  102. package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
  103. package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
  104. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  105. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  106. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  107. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  108. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  109. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  110. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
  111. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  112. package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
  113. package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
  114. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  115. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  116. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  117. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  118. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +885 -0
  119. package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
  120. package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
  121. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
  122. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  123. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
  124. package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
  125. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  126. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  127. package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
  128. package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
  129. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  130. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  131. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  132. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  133. package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
  134. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  135. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  136. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  137. package/Tests/Types/Database/ColumnLength.test.ts +7 -1
  138. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  139. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  140. package/Tests/Types/Decimal.test.ts +37 -0
  141. package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
  142. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
  143. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
  144. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
  145. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
  146. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  147. package/Tests/Types/Port.test.ts +38 -0
  148. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
  149. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  150. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  151. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  152. package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
  153. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  154. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  155. package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
  156. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  157. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  158. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  159. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  160. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  161. package/Types/Database/ColumnLength.ts +9 -1
  162. package/Types/Database/DatabaseProperty.ts +26 -0
  163. package/Types/Database/NumericColumnValue.ts +128 -0
  164. package/Types/Database/TableColumn.ts +11 -0
  165. package/Types/Database/UnsynchronizedIndex.ts +48 -0
  166. package/Types/Decimal.ts +16 -4
  167. package/Types/HashedString.ts +139 -4
  168. package/Types/Permission.ts +48 -0
  169. package/Types/Port.ts +16 -4
  170. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
  171. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  172. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  173. package/UI/Components/Navbar/NavBar.tsx +12 -2
  174. package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
  175. package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
  176. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  177. package/Utils/Dashboard/Components/Index.ts +7 -0
  178. package/Utils/TeamMembersByProject.ts +237 -0
  179. package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
  180. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
  182. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  184. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
  186. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
  188. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
  190. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  192. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  193. package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
  194. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  195. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
  196. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  197. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  198. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  199. package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
  200. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
  202. package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/Project.js +16 -2
  204. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
  206. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/Service.js +28 -1
  208. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  210. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  211. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
  212. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  214. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  216. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/User.js +32 -0
  218. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  219. package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
  220. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  221. package/build/dist/Server/API/BaseAPI.js +11 -1
  222. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  223. package/build/dist/Server/API/DashboardAPI.js +343 -10
  224. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  225. package/build/dist/Server/API/GitHubAPI.js +94 -167
  226. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  227. package/build/dist/Server/API/UserAPI.js +187 -2
  228. package/build/dist/Server/API/UserAPI.js.map +1 -1
  229. package/build/dist/Server/EnvironmentConfig.js +34 -4
  230. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  231. package/build/dist/Server/Infrastructure/GlobalCache.js +120 -0
  232. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  234. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  236. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  237. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
  238. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
  239. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
  240. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
  241. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
  242. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
  243. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
  244. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
  245. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
  246. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
  247. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  248. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  249. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  250. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  251. package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
  252. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  253. package/build/dist/Server/Services/CephClusterService.js +26 -76
  254. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  255. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  256. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  257. package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
  258. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
  259. package/build/dist/Server/Services/DatabaseService.js +246 -2
  260. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  261. package/build/dist/Server/Services/DockerHostService.js +19 -69
  262. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  263. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  264. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  265. package/build/dist/Server/Services/HostService.js +58 -102
  266. package/build/dist/Server/Services/HostService.js.map +1 -1
  267. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  268. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  269. package/build/dist/Server/Services/Index.js +2 -0
  270. package/build/dist/Server/Services/Index.js.map +1 -1
  271. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  272. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  273. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  274. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  275. package/build/dist/Server/Services/LabelService.js +42 -7
  276. package/build/dist/Server/Services/LabelService.js.map +1 -1
  277. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  278. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  279. package/build/dist/Server/Services/MonitorService.js +81 -9
  280. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  281. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  282. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  283. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  284. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  285. package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
  286. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  287. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  288. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  289. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  290. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  291. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  292. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  293. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  294. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  295. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  296. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  297. package/build/dist/Server/Services/ServiceService.js +236 -61
  298. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  299. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  300. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  301. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  302. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  303. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  304. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  305. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  306. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  307. package/build/dist/Server/Services/UserService.js +165 -0
  308. package/build/dist/Server/Services/UserService.js.map +1 -1
  309. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
  310. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
  311. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
  312. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  313. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
  314. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  315. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
  316. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
  317. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  318. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  319. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  320. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  321. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  322. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  323. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  324. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  325. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  326. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  327. package/build/dist/Server/Utils/PasswordHash.js +237 -0
  328. package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
  329. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  330. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  331. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  332. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  333. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  334. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  335. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  336. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  337. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  338. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  339. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  340. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  341. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  342. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  343. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  344. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  345. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  346. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  347. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  348. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  349. package/build/dist/Types/Database/ColumnLength.js +9 -1
  350. package/build/dist/Types/Database/ColumnLength.js.map +1 -1
  351. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  352. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  353. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  354. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  355. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  356. package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
  357. package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
  358. package/build/dist/Types/Decimal.js +14 -3
  359. package/build/dist/Types/Decimal.js.map +1 -1
  360. package/build/dist/Types/HashedString.js +95 -5
  361. package/build/dist/Types/HashedString.js.map +1 -1
  362. package/build/dist/Types/Permission.js +42 -0
  363. package/build/dist/Types/Permission.js.map +1 -1
  364. package/build/dist/Types/Port.js +15 -5
  365. package/build/dist/Types/Port.js.map +1 -1
  366. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
  367. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  368. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  369. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  370. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  371. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  372. package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
  373. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  374. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
  375. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  376. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
  377. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
  378. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  379. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  380. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  381. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  382. package/build/dist/Utils/TeamMembersByProject.js +145 -0
  383. package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
  384. package/package.json +1 -1
@@ -0,0 +1,943 @@
1
+ import Dashboard from "../../../Models/DatabaseModels/Dashboard";
2
+ import DashboardAPI from "../../../Server/API/DashboardAPI";
3
+ import DashboardService from "../../../Server/Services/DashboardService";
4
+ import TelemetryAttributeService from "../../../Server/Services/TelemetryAttributeService";
5
+ import { EncryptionSecret } from "../../../Server/EnvironmentConfig";
6
+ import {
7
+ ExpressRequest,
8
+ ExpressResponse,
9
+ NextFunction,
10
+ } from "../../../Server/Utils/Express";
11
+ import Response from "../../../Server/Utils/Response";
12
+ import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
13
+ import { DashboardVariableType } from "../../../Types/Dashboard/DashboardVariable";
14
+ import DashboardViewConfig from "../../../Types/Dashboard/DashboardViewConfig";
15
+ import BadDataException from "../../../Types/Exception/BadDataException";
16
+ import ForbiddenException from "../../../Types/Exception/ForbiddenException";
17
+ import MasterPasswordRequiredException from "../../../Types/Exception/MasterPasswordRequiredException";
18
+ import NotAuthenticatedException from "../../../Types/Exception/NotAuthenticatedException";
19
+ import NotFoundException from "../../../Types/Exception/NotFoundException";
20
+ import HashedString from "../../../Types/HashedString";
21
+ import { JSONObject } from "../../../Types/JSON";
22
+ import ObjectID from "../../../Types/ObjectID";
23
+ import TelemetryType from "../../../Types/Telemetry/TelemetryType";
24
+ import { mockRouter } from "./Helpers";
25
+ import {
26
+ afterEach,
27
+ beforeAll,
28
+ beforeEach,
29
+ describe,
30
+ expect,
31
+ it,
32
+ } from "@jest/globals";
33
+
34
+ jest.mock("../../../Server/Utils/Express", () => {
35
+ return {
36
+ getRouter: () => {
37
+ return mockRouter;
38
+ },
39
+ };
40
+ });
41
+
42
+ jest.mock("../../../Server/Utils/Response", () => {
43
+ return {
44
+ sendEntityArrayResponse: jest.fn().mockImplementation((...args: []) => {
45
+ return args;
46
+ }),
47
+ sendJsonObjectResponse: jest.fn().mockImplementation((...args: []) => {
48
+ return args;
49
+ }),
50
+ sendEmptySuccessResponse: jest.fn(),
51
+ sendEntityResponse: jest.fn().mockImplementation((...args: []) => {
52
+ return args;
53
+ }),
54
+ sendErrorResponse: jest.fn().mockImplementation((...args: []) => {
55
+ return args;
56
+ }),
57
+ };
58
+ });
59
+
60
+ const ATTRIBUTE_VALUES_ROUTE: string =
61
+ "/dashboard/attribute-values/:dashboardId";
62
+
63
+ const NOT_ON_DASHBOARD_MESSAGE: string =
64
+ "This attribute is not part of this dashboard.";
65
+
66
+ const MASTER_PASSWORD: string = "correct-horse-battery-staple";
67
+
68
+ /*
69
+ * The attribute keys the advisory's proof of concept reaches for: none of
70
+ * them belongs to a dashboard variable, and every one of them is high-value
71
+ * telemetry an anonymous viewer must never be able to enumerate.
72
+ */
73
+ const SENSITIVE_KEYS: Array<string> = [
74
+ "http.url",
75
+ "db.statement",
76
+ "enduser.id",
77
+ "user.email",
78
+ "http.request.header.authorization",
79
+ "k8s.namespace.name",
80
+ ];
81
+
82
+ const ALL_TELEMETRY_TYPES: Array<TelemetryType> = [
83
+ TelemetryType.Metric,
84
+ TelemetryType.Trace,
85
+ TelemetryType.Log,
86
+ TelemetryType.Exception,
87
+ TelemetryType.Profile,
88
+ ];
89
+
90
+ describe("DashboardAPI public attribute-values", () => {
91
+ let dashboardId: ObjectID;
92
+ let projectId: ObjectID;
93
+ let dashboard: Dashboard;
94
+ let mockResponse: ExpressResponse;
95
+ let nextFunction: NextFunction;
96
+
97
+ beforeAll(() => {
98
+ mockRouter.routes.length = 0;
99
+ new DashboardAPI();
100
+ });
101
+
102
+ type VariableSpec = {
103
+ type?: DashboardVariableType | string | undefined;
104
+ attributeKey?: unknown;
105
+ };
106
+
107
+ type BuildViewConfigFunction = (data: {
108
+ variables?: Array<VariableSpec> | undefined;
109
+ componentTypes?: Array<string> | undefined;
110
+ }) => DashboardViewConfig;
111
+
112
+ const buildViewConfig: BuildViewConfigFunction = (data: {
113
+ variables?: Array<VariableSpec> | undefined;
114
+ componentTypes?: Array<string> | undefined;
115
+ }): DashboardViewConfig => {
116
+ return {
117
+ _type: "DashboardViewConfig",
118
+ heightInDashboardUnits: 24,
119
+ components: (data.componentTypes || []).map(
120
+ (componentType: string, index: number): JSONObject => {
121
+ return {
122
+ _type: "DashboardComponent",
123
+ componentId: ObjectID.generate().toString(),
124
+ componentType: componentType,
125
+ topInDashboardUnits: index,
126
+ leftInDashboardUnits: 0,
127
+ widthInDashboardUnits: 12,
128
+ heightInDashboardUnits: 6,
129
+ arguments: {},
130
+ };
131
+ },
132
+ ),
133
+ variables: (data.variables || []).map(
134
+ (variable: VariableSpec, index: number): Record<string, unknown> => {
135
+ return {
136
+ id: ObjectID.generate().toString(),
137
+ name: `variable-${index}`,
138
+ label: `Variable ${index}`,
139
+ type: variable.type,
140
+ attributeKey: variable.attributeKey,
141
+ };
142
+ },
143
+ ),
144
+ } as unknown as DashboardViewConfig;
145
+ };
146
+
147
+ type SetAttributeVariablesFunction = (attributeKeys: Array<string>) => void;
148
+
149
+ /*
150
+ * Put one telemetry-attribute variable on the dashboard per key — the
151
+ * shape the public variable selector actually renders.
152
+ */
153
+ const setAttributeVariables: SetAttributeVariablesFunction = (
154
+ attributeKeys: Array<string>,
155
+ ): void => {
156
+ dashboard.dashboardViewConfig = buildViewConfig({
157
+ componentTypes: [DashboardComponentType.Chart],
158
+ variables: attributeKeys.map((attributeKey: string): VariableSpec => {
159
+ return {
160
+ type: DashboardVariableType.TelemetryAttribute,
161
+ attributeKey: attributeKey,
162
+ };
163
+ }),
164
+ });
165
+ };
166
+
167
+ interface RequestOptions {
168
+ cookies?: Record<string, string> | undefined;
169
+ headers?: Record<string, string> | undefined;
170
+ dashboardIdParam?: string | undefined;
171
+ socket?: JSONObject | undefined;
172
+ ips?: Array<string> | undefined;
173
+ }
174
+
175
+ type CallRouteFunction = (
176
+ body?: JSONObject,
177
+ options?: RequestOptions,
178
+ ) => Promise<void>;
179
+
180
+ const callRoute: CallRouteFunction = async (
181
+ body?: JSONObject,
182
+ options?: RequestOptions,
183
+ ): Promise<void> => {
184
+ const request: ExpressRequest = {
185
+ params: {
186
+ dashboardId:
187
+ options?.dashboardIdParam === undefined
188
+ ? dashboardId.toString()
189
+ : options.dashboardIdParam,
190
+ },
191
+ body: body === undefined ? {} : body,
192
+ query: {},
193
+ cookies: options?.cookies || {},
194
+ headers: options?.headers || {},
195
+ socket: options?.socket || {},
196
+ ips: options?.ips || [],
197
+ } as unknown as ExpressRequest;
198
+
199
+ await mockRouter
200
+ .match("post", ATTRIBUTE_VALUES_ROUTE)
201
+ .handlerFunction(request, mockResponse, nextFunction);
202
+ };
203
+
204
+ type GetFetchArgsFunction = () => JSONObject;
205
+
206
+ const getFetchArgs: GetFetchArgsFunction = (): JSONObject => {
207
+ const calls: Array<Array<unknown>> = (
208
+ TelemetryAttributeService.fetchAttributeValues as jest.Mock
209
+ ).mock.calls as Array<Array<unknown>>;
210
+
211
+ expect(calls.length).toBe(1);
212
+
213
+ return calls[0]![0] as JSONObject;
214
+ };
215
+
216
+ type GetThrownErrorFunction = () => unknown;
217
+
218
+ const getThrownError: GetThrownErrorFunction = (): unknown => {
219
+ const calls: Array<Array<unknown>> = (nextFunction as jest.Mock).mock
220
+ .calls as Array<Array<unknown>>;
221
+
222
+ expect(calls.length).toBe(1);
223
+
224
+ return calls[0]![0];
225
+ };
226
+
227
+ type ExpectNothingReadFunction = () => void;
228
+
229
+ const expectNothingRead: ExpectNothingReadFunction = (): void => {
230
+ expect(
231
+ TelemetryAttributeService.fetchAttributeValues,
232
+ ).not.toHaveBeenCalled();
233
+ };
234
+
235
+ beforeEach(() => {
236
+ jest.clearAllMocks();
237
+
238
+ dashboardId = ObjectID.generate();
239
+ projectId = ObjectID.generate();
240
+
241
+ dashboard = new Dashboard();
242
+ dashboard.id = dashboardId;
243
+ dashboard.projectId = projectId;
244
+ dashboard.isPublicDashboard = true;
245
+ dashboard.enableMasterPassword = false;
246
+ setAttributeVariables([]);
247
+
248
+ jest.spyOn(DashboardService, "findOneById").mockResolvedValue(dashboard);
249
+
250
+ jest
251
+ .spyOn(TelemetryAttributeService, "fetchAttributeValues")
252
+ .mockResolvedValue([]);
253
+
254
+ mockResponse = {
255
+ cookie: jest.fn(),
256
+ send: jest.fn(),
257
+ json: jest.fn(),
258
+ status: jest.fn().mockReturnThis(),
259
+ } as unknown as ExpressResponse;
260
+
261
+ nextFunction = jest.fn();
262
+ });
263
+
264
+ afterEach(() => {
265
+ jest.restoreAllMocks();
266
+ });
267
+
268
+ describe("attribute allowlist (GHSA-w332-x78m-vf3v)", () => {
269
+ it("refuses to read a project's log attributes from a dashboard that has no variables", async () => {
270
+ /*
271
+ * The advisory's proof of concept verbatim: a public dashboard whose
272
+ * only widget is a text box, used as a SELECT DISTINCT over every log
273
+ * in the owning project.
274
+ */
275
+ dashboard.dashboardViewConfig = buildViewConfig({
276
+ componentTypes: [DashboardComponentType.Text],
277
+ });
278
+
279
+ await callRoute({
280
+ attributeKey: "http.url",
281
+ telemetryType: TelemetryType.Log,
282
+ });
283
+
284
+ const error: unknown = getThrownError();
285
+
286
+ expect(error).toBeInstanceOf(BadDataException);
287
+ expect((error as BadDataException).message).toBe(
288
+ NOT_ON_DASHBOARD_MESSAGE,
289
+ );
290
+ expectNothingRead();
291
+ });
292
+
293
+ it("refuses every sensitive key the advisory names, across every telemetry type", async () => {
294
+ setAttributeVariables(["k8s.cluster.name"]);
295
+
296
+ for (const attributeKey of SENSITIVE_KEYS) {
297
+ for (const telemetryType of ALL_TELEMETRY_TYPES) {
298
+ jest.clearAllMocks();
299
+
300
+ await callRoute({
301
+ attributeKey: attributeKey,
302
+ telemetryType: telemetryType,
303
+ });
304
+
305
+ const error: unknown = getThrownError();
306
+
307
+ expect(error).toBeInstanceOf(BadDataException);
308
+ expect((error as BadDataException).message).toBe(
309
+ NOT_ON_DASHBOARD_MESSAGE,
310
+ );
311
+ expectNothingRead();
312
+ }
313
+ }
314
+ });
315
+
316
+ it("serves the key the dashboard's own variable binds to", async () => {
317
+ setAttributeVariables(["k8s.cluster.name"]);
318
+
319
+ await callRoute({ attributeKey: "k8s.cluster.name" });
320
+
321
+ expect(nextFunction).not.toHaveBeenCalled();
322
+ expect(getFetchArgs()["attributeKey"]).toBe("k8s.cluster.name");
323
+ });
324
+
325
+ it("serves every key across several variables, and nothing beside them", async () => {
326
+ const configuredKeys: Array<string> = [
327
+ "resource.k8s.cluster.name",
328
+ "resource.k8s.namespace.name",
329
+ "resource.service.name",
330
+ ];
331
+
332
+ setAttributeVariables(configuredKeys);
333
+
334
+ for (const attributeKey of configuredKeys) {
335
+ jest.clearAllMocks();
336
+
337
+ await callRoute({ attributeKey: attributeKey });
338
+
339
+ expect(nextFunction).not.toHaveBeenCalled();
340
+ expect(getFetchArgs()["attributeKey"]).toBe(attributeKey);
341
+ }
342
+
343
+ jest.clearAllMocks();
344
+
345
+ // A near-miss on a configured key is still a miss.
346
+ await callRoute({ attributeKey: "resource.service.namespace" });
347
+
348
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
349
+ expectNothingRead();
350
+ });
351
+
352
+ it("refuses a key belonging to a different public dashboard's variables", async () => {
353
+ setAttributeVariables(["tenant.a.key"]);
354
+
355
+ await callRoute({ attributeKey: "tenant.b.key" });
356
+
357
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
358
+ expectNothingRead();
359
+ });
360
+
361
+ it("does not let a non-telemetry variable unlock an attribute key", async () => {
362
+ /*
363
+ * Only `Telemetry Attribute` variables drive an attribute lookup. A
364
+ * custom-list / query / text variable that happens to carry an
365
+ * attributeKey field must not widen the allowlist.
366
+ */
367
+ const otherTypes: Array<DashboardVariableType> = [
368
+ DashboardVariableType.CustomList,
369
+ DashboardVariableType.Query,
370
+ DashboardVariableType.TextInput,
371
+ ];
372
+
373
+ for (const variableType of otherTypes) {
374
+ jest.clearAllMocks();
375
+
376
+ dashboard.dashboardViewConfig = buildViewConfig({
377
+ variables: [
378
+ {
379
+ type: variableType,
380
+ attributeKey: "user.email",
381
+ },
382
+ ],
383
+ });
384
+
385
+ await callRoute({ attributeKey: "user.email" });
386
+
387
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
388
+ expectNothingRead();
389
+ }
390
+ });
391
+
392
+ it("ignores a variable whose attribute key is missing or not a string", async () => {
393
+ const badKeys: Array<unknown> = [undefined, null, "", " ", 42, {}, []];
394
+
395
+ for (const badKey of badKeys) {
396
+ jest.clearAllMocks();
397
+
398
+ dashboard.dashboardViewConfig = buildViewConfig({
399
+ variables: [
400
+ {
401
+ type: DashboardVariableType.TelemetryAttribute,
402
+ attributeKey: badKey,
403
+ },
404
+ ],
405
+ });
406
+
407
+ await callRoute({ attributeKey: "http.url" });
408
+
409
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
410
+ expectNothingRead();
411
+ }
412
+ });
413
+
414
+ it("refuses everything when the stored view config is missing or malformed", async () => {
415
+ const malformedConfigs: Array<unknown> = [
416
+ null,
417
+ undefined,
418
+ "not-an-object",
419
+ 42,
420
+ {},
421
+ { variables: null },
422
+ { variables: "not-an-array" },
423
+ { variables: [null, 7, "variable"] },
424
+ { variables: [{ type: DashboardVariableType.TelemetryAttribute }] },
425
+ { components: [{ componentType: DashboardComponentType.Chart }] },
426
+ ];
427
+
428
+ for (const config of malformedConfigs) {
429
+ jest.clearAllMocks();
430
+ dashboard.dashboardViewConfig = config as DashboardViewConfig;
431
+
432
+ await callRoute({ attributeKey: "http.url" });
433
+
434
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
435
+ expectNothingRead();
436
+ }
437
+ });
438
+
439
+ it("matches a configured key regardless of surrounding whitespace", async () => {
440
+ dashboard.dashboardViewConfig = buildViewConfig({
441
+ variables: [
442
+ {
443
+ type: DashboardVariableType.TelemetryAttribute,
444
+ attributeKey: " host.name ",
445
+ },
446
+ ],
447
+ });
448
+
449
+ await callRoute({ attributeKey: " host.name " });
450
+
451
+ expect(nextFunction).not.toHaveBeenCalled();
452
+ expect(getFetchArgs()["attributeKey"]).toBe("host.name");
453
+ });
454
+
455
+ it("does not treat a prototype key as configured", async () => {
456
+ setAttributeVariables(["host.name"]);
457
+
458
+ for (const attributeKey of [
459
+ "__proto__",
460
+ "constructor",
461
+ "toString",
462
+ "hasOwnProperty",
463
+ ]) {
464
+ jest.clearAllMocks();
465
+
466
+ await callRoute({ attributeKey: attributeKey });
467
+
468
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
469
+ expectNothingRead();
470
+ }
471
+ });
472
+
473
+ it("rejects a missing, blank or non-string attribute key before any lookup", async () => {
474
+ setAttributeVariables(["host.name"]);
475
+
476
+ const badRequests: Array<JSONObject> = [
477
+ {},
478
+ { attributeKey: "" },
479
+ { attributeKey: " " },
480
+ { attributeKey: 42 } as unknown as JSONObject,
481
+ { attributeKey: null } as unknown as JSONObject,
482
+ { attributeKey: ["host.name"] } as unknown as JSONObject,
483
+ { attributeKey: { toString: "host.name" } } as unknown as JSONObject,
484
+ ];
485
+
486
+ for (const body of badRequests) {
487
+ jest.clearAllMocks();
488
+
489
+ await callRoute(body);
490
+
491
+ const error: unknown = getThrownError();
492
+
493
+ expect(error).toBeInstanceOf(BadDataException);
494
+ expect((error as BadDataException).message).toBe(
495
+ "attributeKey is required.",
496
+ );
497
+ expectNothingRead();
498
+ }
499
+ });
500
+ });
501
+
502
+ describe("telemetry type", () => {
503
+ it("passes each valid telemetry type through for an allowlisted key", async () => {
504
+ setAttributeVariables(["host.name"]);
505
+
506
+ for (const telemetryType of ALL_TELEMETRY_TYPES) {
507
+ jest.clearAllMocks();
508
+
509
+ await callRoute({
510
+ attributeKey: "host.name",
511
+ telemetryType: telemetryType,
512
+ });
513
+
514
+ expect(nextFunction).not.toHaveBeenCalled();
515
+ expect(getFetchArgs()["telemetryType"]).toBe(telemetryType);
516
+ }
517
+ });
518
+
519
+ it("falls back to Metric for a missing or unrecognised telemetry type", async () => {
520
+ setAttributeVariables(["host.name"]);
521
+
522
+ const badTypes: Array<unknown> = [
523
+ undefined,
524
+ "",
525
+ "Logs",
526
+ "metric",
527
+ "DROP TABLE",
528
+ ];
529
+
530
+ for (const telemetryType of badTypes) {
531
+ jest.clearAllMocks();
532
+
533
+ await callRoute({
534
+ attributeKey: "host.name",
535
+ telemetryType: telemetryType as string,
536
+ });
537
+
538
+ expect(nextFunction).not.toHaveBeenCalled();
539
+ expect(getFetchArgs()["telemetryType"]).toBe(TelemetryType.Metric);
540
+ }
541
+ });
542
+ });
543
+
544
+ /*
545
+ * The load-bearing line of the fix is `dashboardViewConfig: true` in the
546
+ * handler's select. A mock that ignores `select` would keep every test in
547
+ * this file green if that line were deleted — while taking down every real
548
+ * public dashboard — so these pin the projection itself.
549
+ */
550
+ describe("dashboard lookup", () => {
551
+ type GetHandlerLookupFunction = () => JSONObject;
552
+
553
+ const getHandlerLookup: GetHandlerLookupFunction = (): JSONObject => {
554
+ const calls: Array<Array<unknown>> = (
555
+ DashboardService.findOneById as jest.Mock
556
+ ).mock.calls as Array<Array<unknown>>;
557
+
558
+ /*
559
+ * hasReadAccess looks the dashboard up first with its own projection;
560
+ * the handler's is the one that asks for the view config.
561
+ */
562
+ const handlerCall: Array<unknown> | undefined = calls.find(
563
+ (call: Array<unknown>) => {
564
+ const select: JSONObject = (call[0] as JSONObject)[
565
+ "select"
566
+ ] as JSONObject;
567
+
568
+ return Boolean(select && select["dashboardViewConfig"]);
569
+ },
570
+ );
571
+
572
+ expect(handlerCall).toBeDefined();
573
+
574
+ return handlerCall![0] as JSONObject;
575
+ };
576
+
577
+ it("selects the view config, pinned to the path id, as root", async () => {
578
+ setAttributeVariables(["host.name"]);
579
+
580
+ await callRoute({ attributeKey: "host.name" });
581
+
582
+ const lookup: JSONObject = getHandlerLookup();
583
+
584
+ expect(lookup["select"]).toEqual({
585
+ _id: true,
586
+ projectId: true,
587
+ dashboardViewConfig: true,
588
+ });
589
+ expect(lookup["props"]).toEqual({ isRoot: true });
590
+ expect((lookup["id"] as ObjectID).toString()).toBe(
591
+ dashboardId.toString(),
592
+ );
593
+ });
594
+
595
+ it("refuses when the view config was not part of the projection", async () => {
596
+ /*
597
+ * A projection-aware mock: the handler only sees the columns it asked
598
+ * for, the way the real service behaves. Drop `dashboardViewConfig`
599
+ * from the handler's select and this test fails instead of silently
600
+ * allowlisting nothing.
601
+ */
602
+ const source: Dashboard = dashboard;
603
+
604
+ setAttributeVariables(["host.name"]);
605
+
606
+ jest
607
+ .spyOn(DashboardService, "findOneById")
608
+ .mockImplementation(async (data: any) => {
609
+ const projected: Dashboard = new Dashboard();
610
+
611
+ for (const column of Object.keys((data.select || {}) as JSONObject)) {
612
+ (projected as unknown as Record<string, unknown>)[column] = (
613
+ source as unknown as Record<string, unknown>
614
+ )[column];
615
+ }
616
+
617
+ projected.id = source.id;
618
+
619
+ return projected;
620
+ });
621
+
622
+ await callRoute({ attributeKey: "host.name" });
623
+
624
+ expect(nextFunction).not.toHaveBeenCalled();
625
+ expect(
626
+ TelemetryAttributeService.fetchAttributeValues,
627
+ ).toHaveBeenCalledTimes(1);
628
+ });
629
+
630
+ it("fails closed when the dashboard lookup throws", async () => {
631
+ jest
632
+ .spyOn(DashboardService, "findOneById")
633
+ .mockRejectedValue(new Error("connection reset"));
634
+
635
+ await callRoute({ attributeKey: "host.name" });
636
+
637
+ expect(getThrownError()).toBeInstanceOf(NotAuthenticatedException);
638
+ expectNothingRead();
639
+ });
640
+
641
+ /*
642
+ * ObjectID does not validate its input, so a malformed path id is not
643
+ * rejected up front — it simply matches no row. Pin that this stays
644
+ * fail-closed: an id-aware lookup means the caller gets "no access"
645
+ * and no telemetry is read, rather than the id being passed through to
646
+ * a query.
647
+ */
648
+ it("refuses a dashboardId that matches no dashboard, malformed or not", async () => {
649
+ setAttributeVariables(["host.name"]);
650
+
651
+ const realDashboard: Dashboard = dashboard;
652
+
653
+ jest
654
+ .spyOn(DashboardService, "findOneById")
655
+ .mockImplementation(async (data: any) => {
656
+ return data.id?.toString() === dashboardId.toString()
657
+ ? realDashboard
658
+ : null;
659
+ });
660
+
661
+ for (const badId of [
662
+ "not-an-object-id",
663
+ "' OR 1=1--",
664
+ "",
665
+ ObjectID.generate().toString(),
666
+ ]) {
667
+ jest.clearAllMocks();
668
+
669
+ await callRoute(
670
+ { attributeKey: "host.name" },
671
+ { dashboardIdParam: badId },
672
+ );
673
+
674
+ expect(getThrownError()).toBeInstanceOf(NotAuthenticatedException);
675
+ expectNothingRead();
676
+ }
677
+ });
678
+ });
679
+
680
+ describe("authorization", () => {
681
+ it("rejects a dashboard that is not public before reading anything", async () => {
682
+ dashboard.isPublicDashboard = false;
683
+ setAttributeVariables(["host.name"]);
684
+
685
+ await callRoute({ attributeKey: "host.name" });
686
+
687
+ expect(getThrownError()).toBeInstanceOf(NotAuthenticatedException);
688
+ expectNothingRead();
689
+ });
690
+
691
+ it("refuses a password-protected dashboard with no cookie", async () => {
692
+ setAttributeVariables(["host.name"]);
693
+ dashboard.enableMasterPassword = true;
694
+ dashboard.masterPassword = new HashedString(
695
+ await HashedString.hashValue(MASTER_PASSWORD, EncryptionSecret),
696
+ true,
697
+ );
698
+
699
+ await callRoute({ attributeKey: "host.name" });
700
+
701
+ expect(getThrownError()).toBeInstanceOf(MasterPasswordRequiredException);
702
+ expectNothingRead();
703
+ });
704
+
705
+ it("serves an allowlisted key once the master-password cookie is presented", async () => {
706
+ setAttributeVariables(["host.name"]);
707
+ dashboard.enableMasterPassword = true;
708
+ dashboard.masterPassword = new HashedString(
709
+ await HashedString.hashValue(MASTER_PASSWORD, EncryptionSecret),
710
+ true,
711
+ );
712
+
713
+ /*
714
+ * Mint the cookie through the real unlock route rather than forging a
715
+ * JWT — hasValidMasterPasswordCookie does a genuine jwt.verify.
716
+ */
717
+ await mockRouter
718
+ .match("post", "/dashboard/master-password/:dashboardId")
719
+ .handlerFunction(
720
+ {
721
+ params: { dashboardId: dashboardId.toString() },
722
+ body: { password: MASTER_PASSWORD },
723
+ cookies: {},
724
+ headers: {},
725
+ socket: {},
726
+ ips: [],
727
+ } as unknown as ExpressRequest,
728
+ mockResponse,
729
+ nextFunction,
730
+ );
731
+
732
+ const cookieCall: Array<unknown> = (mockResponse.cookie as jest.Mock).mock
733
+ .calls[0] as Array<unknown>;
734
+
735
+ expect(cookieCall).toBeDefined();
736
+
737
+ await callRoute(
738
+ { attributeKey: "host.name" },
739
+ {
740
+ cookies: {
741
+ [cookieCall[0] as string]: cookieCall[1] as string,
742
+ },
743
+ },
744
+ );
745
+
746
+ expect(nextFunction).not.toHaveBeenCalled();
747
+ expect(getFetchArgs()["attributeKey"]).toBe("host.name");
748
+ });
749
+
750
+ it("fails closed when protection is enabled but no password was ever set", async () => {
751
+ setAttributeVariables(["host.name"]);
752
+ dashboard.enableMasterPassword = true;
753
+ delete dashboard.masterPassword;
754
+
755
+ await callRoute({ attributeKey: "host.name" });
756
+
757
+ expect(getThrownError()).toBeInstanceOf(MasterPasswordRequiredException);
758
+ expectNothingRead();
759
+ });
760
+
761
+ it("serves a whitelisted IP", async () => {
762
+ setAttributeVariables(["host.name"]);
763
+ dashboard.ipWhitelist = "10.0.0.0/8\n203.0.113.7";
764
+
765
+ await callRoute(
766
+ { attributeKey: "host.name" },
767
+ { headers: { "x-forwarded-for": "203.0.113.7" } },
768
+ );
769
+
770
+ expect(nextFunction).not.toHaveBeenCalled();
771
+ expect(getFetchArgs()["attributeKey"]).toBe("host.name");
772
+ });
773
+
774
+ it("refuses a blocked IP before reading any telemetry", async () => {
775
+ setAttributeVariables(["host.name"]);
776
+ dashboard.ipWhitelist = "10.0.0.0/8\n203.0.113.7";
777
+
778
+ await callRoute(
779
+ { attributeKey: "host.name" },
780
+ { headers: { "x-forwarded-for": "198.51.100.5" } },
781
+ );
782
+
783
+ expect(getThrownError()).toBeInstanceOf(ForbiddenException);
784
+ expectNothingRead();
785
+ });
786
+
787
+ it("refuses when the client IP cannot be determined at all", async () => {
788
+ setAttributeVariables(["host.name"]);
789
+ dashboard.ipWhitelist = "203.0.113.7";
790
+
791
+ await callRoute({ attributeKey: "host.name" });
792
+
793
+ expect(getThrownError()).toBeInstanceOf(ForbiddenException);
794
+ expectNothingRead();
795
+ });
796
+ });
797
+
798
+ describe("response", () => {
799
+ it("returns exactly the values the telemetry service found", async () => {
800
+ setAttributeVariables(["host.name"]);
801
+ jest
802
+ .spyOn(TelemetryAttributeService, "fetchAttributeValues")
803
+ .mockResolvedValue(["prod", "staging"]);
804
+
805
+ await callRoute({ attributeKey: "host.name" });
806
+
807
+ expect(Response.sendJsonObjectResponse).toHaveBeenCalledTimes(1);
808
+
809
+ const responseCall: Array<unknown> = (
810
+ Response.sendJsonObjectResponse as jest.Mock
811
+ ).mock.calls[0] as Array<unknown>;
812
+
813
+ expect(responseCall[2]).toEqual({ values: ["prod", "staging"] });
814
+ });
815
+
816
+ it("hands the telemetry service exactly three arguments", async () => {
817
+ setAttributeVariables(["host.name"]);
818
+
819
+ await callRoute({
820
+ attributeKey: "host.name",
821
+ searchText: "%",
822
+ metricName: "billing.revenue",
823
+ limit: 999999,
824
+ projectId: ObjectID.generate().toString(),
825
+ extra: 1,
826
+ });
827
+
828
+ expect(Object.keys(getFetchArgs()).sort()).toEqual([
829
+ "attributeKey",
830
+ "projectId",
831
+ "telemetryType",
832
+ ]);
833
+ });
834
+
835
+ it("sends no response at all when the key is refused", async () => {
836
+ setAttributeVariables(["host.name"]);
837
+
838
+ await callRoute({ attributeKey: "user.email" });
839
+
840
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
841
+ });
842
+ });
843
+
844
+ describe("normalization", () => {
845
+ it("matches attribute keys case-sensitively", async () => {
846
+ // ClickHouse map subscripts are case-sensitive; the allowlist must be too.
847
+ setAttributeVariables(["host.name"]);
848
+
849
+ for (const attributeKey of ["Host.Name", "HOST.NAME", "Host.name"]) {
850
+ jest.clearAllMocks();
851
+
852
+ await callRoute({ attributeKey: attributeKey });
853
+
854
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
855
+ expectNothingRead();
856
+ }
857
+ });
858
+
859
+ it("does not treat a key that merely contains a configured key as configured", async () => {
860
+ setAttributeVariables(["host.name"]);
861
+
862
+ for (const attributeKey of [
863
+ "host.name.extra",
864
+ "prefix.host.name",
865
+ "host_name",
866
+ ]) {
867
+ jest.clearAllMocks();
868
+
869
+ await callRoute({ attributeKey: attributeKey });
870
+
871
+ expect(getThrownError()).toBeInstanceOf(BadDataException);
872
+ expectNothingRead();
873
+ }
874
+ });
875
+ });
876
+
877
+ describe("dashboard existence", () => {
878
+ /*
879
+ * A missing dashboard never reaches the attribute lookup: hasReadAccess
880
+ * fails closed first, so the viewer is told they have no access rather
881
+ * than whether the id exists.
882
+ */
883
+ it("rejects a dashboard that cannot be found", async () => {
884
+ jest.spyOn(DashboardService, "findOneById").mockResolvedValue(null);
885
+
886
+ await callRoute({ attributeKey: "host.name" });
887
+
888
+ expect(getThrownError()).toBeInstanceOf(NotAuthenticatedException);
889
+ expectNothingRead();
890
+ });
891
+
892
+ it("rejects a dashboard with no project", async () => {
893
+ setAttributeVariables(["host.name"]);
894
+ delete dashboard.projectId;
895
+
896
+ await callRoute({ attributeKey: "host.name" });
897
+
898
+ expect(getThrownError()).toBeInstanceOf(NotFoundException);
899
+ expectNothingRead();
900
+ });
901
+ });
902
+
903
+ describe("project scoping", () => {
904
+ it("pins the lookup to the dashboard's own project", async () => {
905
+ setAttributeVariables(["host.name"]);
906
+
907
+ await callRoute({ attributeKey: "host.name" });
908
+
909
+ expect(getFetchArgs()["projectId"]).toBe(projectId);
910
+ });
911
+
912
+ it("ignores a client-supplied projectId pointing at another tenant", async () => {
913
+ const otherProjectId: ObjectID = ObjectID.generate();
914
+
915
+ setAttributeVariables(["host.name"]);
916
+
917
+ await callRoute({
918
+ attributeKey: "host.name",
919
+ projectId: otherProjectId.toString(),
920
+ });
921
+
922
+ const fetchArgs: JSONObject = getFetchArgs();
923
+
924
+ expect(fetchArgs["projectId"]).toBe(projectId);
925
+ expect(fetchArgs["projectId"]).not.toBe(otherProjectId.toString());
926
+ });
927
+
928
+ it("never forwards a client-supplied search text or metric scope", async () => {
929
+ setAttributeVariables(["host.name"]);
930
+
931
+ await callRoute({
932
+ attributeKey: "host.name",
933
+ searchText: "%",
934
+ metricName: "some.other.metric",
935
+ });
936
+
937
+ const fetchArgs: JSONObject = getFetchArgs();
938
+
939
+ expect(fetchArgs["searchText"]).toBeUndefined();
940
+ expect(fetchArgs["metricName"]).toBeUndefined();
941
+ });
942
+ });
943
+ });