@oneuptime/common 14.0.0 → 14.0.1

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 (196) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  3. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  4. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  5. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  9. package/Server/Services/DatabaseService.ts +10 -0
  10. package/Server/Services/Index.ts +4 -0
  11. package/Server/Services/MonitorService.ts +3 -2
  12. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  13. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  14. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  15. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  16. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  17. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  18. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  19. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  20. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  21. package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
  25. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  26. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  27. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  28. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  29. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  30. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  31. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  32. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  33. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  34. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  35. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  36. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  37. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  38. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  39. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  40. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  41. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  42. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  43. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  44. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  45. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  46. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  47. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  48. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  49. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  50. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  51. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  52. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  53. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  54. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  55. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  56. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  57. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  58. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  59. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  60. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  61. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  62. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  63. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  64. package/Tests/Server/Types/Markdown.test.ts +2 -1
  65. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  66. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  67. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  68. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  69. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  71. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  72. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  73. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  74. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  75. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  76. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  77. package/Tests/Types/API/Route.test.ts +56 -0
  78. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  79. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  80. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  81. package/Tests/UI/Components/Icon.test.tsx +26 -0
  82. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  83. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  84. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  85. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  86. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  87. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  88. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  89. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  90. package/Types/API/Route.ts +19 -2
  91. package/Types/API/URL.ts +26 -2
  92. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  93. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  94. package/Types/Permission.ts +90 -0
  95. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  96. package/Types/Rules/RuleRun.ts +4 -0
  97. package/UI/Components/Feed/FeedOptions.ts +427 -0
  98. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  99. package/UI/Components/Feed/useFeedItems.ts +28 -9
  100. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  101. package/UI/Components/Icon/Icon.tsx +1 -1
  102. package/Utils/LabelRuleImportExport.ts +27 -9
  103. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  104. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  105. package/Utils/Slo/SloDuration.ts +22 -6
  106. package/Utils/Slo/SloHealth.ts +8 -7
  107. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  108. package/Utils/Slo/SloOverviewText.ts +33 -0
  109. package/Utils/Slo/SloProjection.ts +4 -3
  110. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  111. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  113. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  115. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  116. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  117. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +7 -0
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/Index.js +4 -0
  129. package/build/dist/Server/Services/Index.js.map +1 -1
  130. package/build/dist/Server/Services/MonitorService.js +3 -2
  131. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  132. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  133. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  134. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  135. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  136. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  137. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  138. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  139. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  140. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  141. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  142. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  143. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  144. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  145. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  146. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  147. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  148. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  150. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
  151. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
  152. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  154. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
  155. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  156. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  157. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  158. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  159. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  160. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  161. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  162. package/build/dist/Types/API/Route.js +18 -2
  163. package/build/dist/Types/API/Route.js.map +1 -1
  164. package/build/dist/Types/API/URL.js +23 -2
  165. package/build/dist/Types/API/URL.js.map +1 -1
  166. package/build/dist/Types/Permission.js +84 -0
  167. package/build/dist/Types/Permission.js.map +1 -1
  168. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  169. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  170. package/build/dist/Types/Rules/RuleRun.js +4 -0
  171. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  172. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  173. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  174. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  175. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  176. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  177. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  178. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  179. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  180. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  181. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  182. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  183. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  184. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  185. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  186. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  187. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  188. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  189. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  190. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  191. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  192. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  193. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  194. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  195. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  196. package/package.json +1 -1
@@ -363,3 +363,157 @@ describe("RuleRunPermission.assertCanRun", () => {
363
363
  }).toThrow(new BadDataException("This rule cannot be run."));
364
364
  });
365
365
  });
366
+
367
+ /*
368
+ * SLO label and owner rules walk every SLO in the project as root, so running
369
+ * one needs the rule's edit permission, EditServiceLevelObjective, and - for
370
+ * owner rules - create on both SLO owner tables. Read off the real models'
371
+ * @TableAccessControl, like everything above.
372
+ */
373
+ describe("RuleRunPermission.assertCanRun - SLO label and owner rules", () => {
374
+ it("lets a caller who can edit the rule and every SLO run an SLO label rule", () => {
375
+ expect(() => {
376
+ assertCanRun(
377
+ RuleRunType.ServiceLevelObjectiveLabelRule,
378
+ propsWith({
379
+ permissions: [
380
+ Permission.EditServiceLevelObjectiveLabelRule,
381
+ Permission.EditServiceLevelObjective,
382
+ ],
383
+ }),
384
+ );
385
+ }).not.toThrow();
386
+ });
387
+
388
+ it("refuses a caller who can edit SLOs but not the rule", () => {
389
+ expect(() => {
390
+ assertCanRun(
391
+ RuleRunType.ServiceLevelObjectiveLabelRule,
392
+ propsWith({
393
+ permissions: [
394
+ Permission.EditServiceLevelObjective,
395
+ // Another SLO rule's permission is not this one's.
396
+ Permission.EditServiceLevelObjectiveMonitorRule,
397
+ ],
398
+ }),
399
+ );
400
+ }).toThrow(
401
+ "You do not have permission to edit this rule, which running it requires.",
402
+ );
403
+ });
404
+
405
+ it("refuses a rule author who cannot edit SLOs, naming them", () => {
406
+ expect(() => {
407
+ assertCanRun(
408
+ RuleRunType.ServiceLevelObjectiveLabelRule,
409
+ propsWith({
410
+ permissions: [Permission.EditServiceLevelObjectiveLabelRule],
411
+ }),
412
+ );
413
+ }).toThrow(
414
+ "You do not have permission to edit every SLO in this project, which running this rule does.",
415
+ );
416
+ });
417
+
418
+ it("does not let read access to the rule stand in for edit", () => {
419
+ expect(() => {
420
+ assertCanRun(
421
+ RuleRunType.ServiceLevelObjectiveOwnerRule,
422
+ propsWith({
423
+ permissions: [
424
+ Permission.ReadServiceLevelObjectiveOwnerRule,
425
+ Permission.EditServiceLevelObjective,
426
+ Permission.CreateServiceLevelObjectiveOwnerUser,
427
+ Permission.CreateServiceLevelObjectiveOwnerTeam,
428
+ ],
429
+ }),
430
+ );
431
+ }).toThrow(NotAuthorizedException);
432
+ });
433
+
434
+ it("requires create on both SLO owner tables for an SLO owner rule", () => {
435
+ const ruleAndSlo: Array<Permission> = [
436
+ Permission.EditServiceLevelObjectiveOwnerRule,
437
+ Permission.EditServiceLevelObjective,
438
+ ];
439
+
440
+ expect(() => {
441
+ assertCanRun(
442
+ RuleRunType.ServiceLevelObjectiveOwnerRule,
443
+ propsWith({ permissions: ruleAndSlo }),
444
+ );
445
+ }).toThrow(
446
+ "You do not have permission to add owners to SLOs, which running this rule does.",
447
+ );
448
+
449
+ expect(() => {
450
+ assertCanRun(
451
+ RuleRunType.ServiceLevelObjectiveOwnerRule,
452
+ propsWith({
453
+ permissions: [
454
+ ...ruleAndSlo,
455
+ Permission.CreateServiceLevelObjectiveOwnerUser,
456
+ ],
457
+ }),
458
+ );
459
+ }).toThrow(NotAuthorizedException);
460
+
461
+ expect(() => {
462
+ assertCanRun(
463
+ RuleRunType.ServiceLevelObjectiveOwnerRule,
464
+ propsWith({
465
+ permissions: [
466
+ ...ruleAndSlo,
467
+ Permission.CreateServiceLevelObjectiveOwnerUser,
468
+ Permission.CreateServiceLevelObjectiveOwnerTeam,
469
+ ],
470
+ }),
471
+ );
472
+ }).not.toThrow();
473
+ });
474
+
475
+ /*
476
+ * A run reaches every SLO, so an edit grant limited to the SLOs a user owns
477
+ * cannot authorize one.
478
+ */
479
+ it("does not count an Owned-scoped SLO edit grant", () => {
480
+ expect(() => {
481
+ assertCanRun(
482
+ RuleRunType.ServiceLevelObjectiveLabelRule,
483
+ propsWith({
484
+ permissions: [
485
+ Permission.EditServiceLevelObjectiveLabelRule,
486
+ Permission.EditServiceLevelObjective,
487
+ ],
488
+ scope: PermissionScope.Owned,
489
+ }),
490
+ );
491
+ }).toThrow(NotAuthorizedException);
492
+ });
493
+
494
+ it("honours a team's block list on SLOs, even over an admin grant", () => {
495
+ expect(() => {
496
+ assertCanRun(
497
+ RuleRunType.ServiceLevelObjectiveLabelRule,
498
+ propsWith({
499
+ permissions: [Permission.ProjectAdmin],
500
+ blockedPermissions: [Permission.EditServiceLevelObjective],
501
+ }),
502
+ );
503
+ }).toThrow(/permission block list/);
504
+ });
505
+
506
+ it("lets a project admin run both", () => {
507
+ for (const ruleType of [
508
+ RuleRunType.ServiceLevelObjectiveLabelRule,
509
+ RuleRunType.ServiceLevelObjectiveOwnerRule,
510
+ ]) {
511
+ expect(() => {
512
+ assertCanRun(
513
+ ruleType,
514
+ propsWith({ permissions: [Permission.ProjectAdmin] }),
515
+ );
516
+ }).not.toThrow();
517
+ }
518
+ });
519
+ });
@@ -191,3 +191,114 @@ describe("SSRFProtection.validateWebhookTargetIsSafe", () => {
191
191
  });
192
192
  });
193
193
  });
194
+
195
+ /*
196
+ * The guard parses the target with OneUptime's URL type as well as WHATWG, and
197
+ * turned any parse failure into "<label> is not a valid URL". That parser
198
+ * refused a first path segment containing a ":" - every Telegram Bot API URL,
199
+ * because the token is that segment - and any path containing "~". So the
200
+ * sandboxed axios bridge refused perfectly public requests as invalid URLs.
201
+ */
202
+ describe("SSRFProtection with a ':' or '~' in the path", () => {
203
+ const BOT_TOKEN: string = "8000000001:AAFakeFakeFakeFakeFakeFakeFake_-12345";
204
+ const TELEGRAM_URL: string = `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`;
205
+ let lookupSpy: LookupSpy;
206
+
207
+ beforeEach(() => {
208
+ lookupSpy = jest.spyOn(dns.promises, "lookup") as unknown as LookupSpy;
209
+ lookupSpy.mockResolvedValue([{ address: "149.154.167.220", family: 4 }]);
210
+ });
211
+
212
+ afterEach(() => {
213
+ jest.restoreAllMocks();
214
+ });
215
+
216
+ test("validateWebhookTargetIsSafe accepts a Telegram Bot API URL", async () => {
217
+ await expect(
218
+ SSRFProtection.validateWebhookTargetIsSafe(TELEGRAM_URL, {
219
+ targetLabel: "Request URL",
220
+ }),
221
+ ).resolves.toBeUndefined();
222
+
223
+ expect(lookupSpy).toHaveBeenCalledWith("api.telegram.org", { all: true });
224
+ });
225
+
226
+ test("validateWebhookTargetIsSafe accepts the same URL as a URL object", async () => {
227
+ await expect(
228
+ SSRFProtection.validateWebhookTargetIsSafe(URL.fromString(TELEGRAM_URL)),
229
+ ).resolves.toBeUndefined();
230
+ });
231
+
232
+ test("validateAndResolveWebhookTarget keeps the token path and pins the address", async () => {
233
+ const target: Awaited<
234
+ ReturnType<typeof SSRFProtection.validateAndResolveWebhookTarget>
235
+ > = await SSRFProtection.validateAndResolveWebhookTarget(TELEGRAM_URL, {
236
+ targetLabel: "Request URL",
237
+ });
238
+
239
+ expect(target.url.toString()).toBe(TELEGRAM_URL);
240
+ expect(target.url.pathname).toBe(`/bot${BOT_TOKEN}/sendMessage`);
241
+ expect(target.addresses).toEqual([
242
+ { address: "149.154.167.220", family: 4 },
243
+ ]);
244
+ });
245
+
246
+ test.each([
247
+ "https://example.com/~user/hook",
248
+ "https://example.com/urn:uuid:6e8bc430-9c3a/x",
249
+ "https://example.com/abc:def",
250
+ ])("accepts the public URL %s", async (rawUrl: string) => {
251
+ await expect(
252
+ SSRFProtection.validateAndResolveWebhookTarget(rawUrl, {
253
+ targetLabel: "Request URL",
254
+ }),
255
+ ).resolves.toMatchObject({ url: new globalThis.URL(rawUrl) });
256
+ });
257
+
258
+ test.each([
259
+ [
260
+ "the metadata endpoint",
261
+ `http://169.254.169.254/bot${BOT_TOKEN}/sendMessage`,
262
+ ],
263
+ ["loopback", `http://127.0.0.1:8080/bot${BOT_TOKEN}/sendMessage`],
264
+ ["an RFC-1918 address", `http://10.0.0.5/bot${BOT_TOKEN}/sendMessage`],
265
+ ["IPv6 loopback", `http://[::1]/bot${BOT_TOKEN}/sendMessage`],
266
+ ])(
267
+ "still refuses %s behind a token path, for its host",
268
+ async (_label: string, rawUrl: string) => {
269
+ const validation: Promise<unknown> =
270
+ SSRFProtection.validateAndResolveWebhookTarget(rawUrl, {
271
+ targetLabel: "Request URL",
272
+ });
273
+
274
+ await expect(validation).rejects.toThrow(/private|loopback|link-local/);
275
+ await expect(validation).rejects.not.toThrow(/not a valid URL/);
276
+ },
277
+ );
278
+
279
+ test("still refuses a public name that resolves internally behind a token path", async () => {
280
+ lookupSpy.mockResolvedValue([{ address: "169.254.169.254", family: 4 }]);
281
+
282
+ await expect(
283
+ SSRFProtection.validateAndResolveWebhookTarget(TELEGRAM_URL, {
284
+ targetLabel: "Request URL",
285
+ }),
286
+ ).rejects.toThrow(/resolves to a private, loopback, or link-local/);
287
+ });
288
+
289
+ test("a refusal does not quote the token", async () => {
290
+ let message: string = "";
291
+
292
+ try {
293
+ await SSRFProtection.validateAndResolveWebhookTarget(
294
+ `http://169.254.169.254/bot${BOT_TOKEN}/sendMessage`,
295
+ { targetLabel: "Request URL" },
296
+ );
297
+ } catch (err) {
298
+ message = (err as Error).message;
299
+ }
300
+
301
+ expect(message).not.toBe("");
302
+ expect(message).not.toContain(BOT_TOKEN);
303
+ });
304
+ });
@@ -0,0 +1,67 @@
1
+ import SloRulePatternValidator from "../../../../Server/Utils/Slo/SloRulePatternValidator";
2
+ import BadDataException from "../../../../Types/Exception/BadDataException";
3
+ import { describe, expect, it } from "@jest/globals";
4
+
5
+ /*
6
+ * Write-time pattern validation for SLO label and owner rules: the engines
7
+ * take a case-insensitive regex or a '*' wildcard, and anything else could
8
+ * only ever match nothing.
9
+ */
10
+ describe("SloRulePatternValidator", () => {
11
+ it.each([
12
+ ["an anchored regex", "^checkout-.*$"],
13
+ ["an alternation", "checkout|payments"],
14
+ ["a leading wildcard", "*checkout"],
15
+ ["a wildcard on both sides", "*checkout*"],
16
+ ["a plain word", "checkout"],
17
+ // Not a regex, but the '*' makes it a wildcard.
18
+ ["a wildcard that is not a valid regex", "*api-(01*"],
19
+ ])("accepts %s", (_: string, pattern: string) => {
20
+ expect(() => {
21
+ SloRulePatternValidator.validate({
22
+ namePattern: pattern,
23
+ descriptionPattern: pattern,
24
+ });
25
+ }).not.toThrow();
26
+ });
27
+
28
+ it.each([undefined, null, ""])(
29
+ "treats %p as no pattern at all",
30
+ (pattern: string | null | undefined) => {
31
+ expect(() => {
32
+ SloRulePatternValidator.validate({
33
+ namePattern: pattern,
34
+ descriptionPattern: pattern,
35
+ });
36
+ }).not.toThrow();
37
+ },
38
+ );
39
+
40
+ it("refuses a name pattern that is neither a regex nor a wildcard", () => {
41
+ expect(() => {
42
+ SloRulePatternValidator.validate({ namePattern: "checkout-(01" });
43
+ }).toThrow(
44
+ new BadDataException(
45
+ `SLO Name Pattern "checkout-(01" is not a valid regular expression, and contains no '*' wildcard to fall back on, so it would never match an SLO. Use a regex such as checkout-.* or a wildcard such as *checkout*.`,
46
+ ),
47
+ );
48
+ });
49
+
50
+ it("refuses a description pattern that is neither a regex nor a wildcard", () => {
51
+ expect(() => {
52
+ SloRulePatternValidator.validate({
53
+ namePattern: "checkout",
54
+ descriptionPattern: "[unclosed",
55
+ });
56
+ }).toThrow('SLO Description Pattern "[unclosed"');
57
+ });
58
+
59
+ it("checks the name before the description", () => {
60
+ expect(() => {
61
+ SloRulePatternValidator.validate({
62
+ namePattern: "(",
63
+ descriptionPattern: "[",
64
+ });
65
+ }).toThrow("SLO Name Pattern");
66
+ });
67
+ });
@@ -1365,3 +1365,216 @@ describe("VMRunner sandbox axios bridge — URL absolutization", () => {
1365
1365
  },
1366
1366
  );
1367
1367
  });
1368
+
1369
+ /*
1370
+ * The SSRF guard parses the target with OneUptime's URL type as well as
1371
+ * WHATWG. That parser refused any URL whose first path segment contained a
1372
+ * ":" - which every Telegram Bot API URL has, because the token
1373
+ * ("<id>:<secret>") is that segment - and any path containing "~". The guard
1374
+ * turned the parse failure into "Request URL is not a valid URL", so sandboxed
1375
+ * code could not call Telegram at all. These drive the real isolate against a
1376
+ * loopback server to prove the request now reaches the wire unchanged.
1377
+ */
1378
+ describe("VMRunner sandbox axios bridge — paths with ':' or '~'", () => {
1379
+ const BOT_TOKEN: string = "8000000001:AAFakeFakeFakeFakeFakeFakeFake_-12345";
1380
+ const TELEGRAM_PATH: string = `/bot${BOT_TOKEN}/sendMessage`;
1381
+
1382
+ interface EchoedRequest {
1383
+ method: string;
1384
+ url: string;
1385
+ host: string;
1386
+ body: string;
1387
+ }
1388
+
1389
+ async function startEchoServer(): Promise<RunningLoopbackServer> {
1390
+ return startLoopbackServer(
1391
+ (request: http.IncomingMessage, response: http.ServerResponse): void => {
1392
+ let body: string = "";
1393
+ request.on("data", (chunk: Buffer): void => {
1394
+ body += chunk.toString("utf8");
1395
+ });
1396
+ request.on("end", (): void => {
1397
+ const echoed: EchoedRequest = {
1398
+ method: request.method || "",
1399
+ url: request.url || "",
1400
+ host: request.headers.host || "",
1401
+ body: body,
1402
+ };
1403
+ response.setHeader("Content-Type", "application/json");
1404
+ response.end(JSON.stringify(echoed));
1405
+ });
1406
+ },
1407
+ );
1408
+ }
1409
+
1410
+ async function runAgainstLoopback(code: string): Promise<EchoedRequest> {
1411
+ const result: ReturnResult = await runInSandbox(code, {
1412
+ allowPrivateNetworkRequests: true,
1413
+ });
1414
+
1415
+ expect(result.scriptError).toBeUndefined();
1416
+
1417
+ return result.returnValue as EchoedRequest;
1418
+ }
1419
+
1420
+ beforeEach(() => {
1421
+ process.env[PRIVATE_NETWORK_WEBHOOK_ALLOWLIST_ENV] = "127.0.0.1";
1422
+ });
1423
+
1424
+ test("axios.post reaches a Telegram /bot<id>:<secret>/sendMessage path", async () => {
1425
+ const echoServer: RunningLoopbackServer = await startEchoServer();
1426
+
1427
+ try {
1428
+ const echoed: EchoedRequest = await runAgainstLoopback(`
1429
+ const response = await axios.post(
1430
+ 'http://127.0.0.1:${echoServer.port}${TELEGRAM_PATH}',
1431
+ { chat_id: '@mychannel', text: 'Deploy finished' }
1432
+ );
1433
+ return response.data;
1434
+ `);
1435
+
1436
+ expect(echoed.method).toBe("POST");
1437
+ expect(echoed.url).toBe(TELEGRAM_PATH);
1438
+ expect(JSON.parse(echoed.body)).toEqual({
1439
+ chat_id: "@mychannel",
1440
+ text: "Deploy finished",
1441
+ });
1442
+ expect(echoServer.requestCount()).toBe(1);
1443
+ } finally {
1444
+ await echoServer.close();
1445
+ }
1446
+ });
1447
+
1448
+ test.each([
1449
+ ["get", "axios.get(URL)"],
1450
+ ["put", "axios.put(URL, { a: 1 })"],
1451
+ ["patch", "axios.patch(URL, { a: 1 })"],
1452
+ ["delete", "axios.delete(URL)"],
1453
+ ["post", "axios.request({ method: 'post', url: URL, data: { a: 1 } })"],
1454
+ ["get", "axios(URL)"],
1455
+ ])(
1456
+ "axios.%s via %s reaches the same path",
1457
+ async (method: string, call: string) => {
1458
+ const echoServer: RunningLoopbackServer = await startEchoServer();
1459
+
1460
+ try {
1461
+ const echoed: EchoedRequest = await runAgainstLoopback(`
1462
+ const URL = 'http://127.0.0.1:${echoServer.port}${TELEGRAM_PATH}';
1463
+ const response = await ${call};
1464
+ return response.data;
1465
+ `);
1466
+
1467
+ expect(echoed.method).toBe(method.toUpperCase());
1468
+ expect(echoed.url).toBe(TELEGRAM_PATH);
1469
+ } finally {
1470
+ await echoServer.close();
1471
+ }
1472
+ },
1473
+ );
1474
+
1475
+ test("keeps the query string on a Telegram path", async () => {
1476
+ const echoServer: RunningLoopbackServer = await startEchoServer();
1477
+
1478
+ try {
1479
+ const echoed: EchoedRequest = await runAgainstLoopback(`
1480
+ const response = await axios.get(
1481
+ 'http://127.0.0.1:${echoServer.port}${TELEGRAM_PATH}?chat_id=%40mychannel&text=hi'
1482
+ );
1483
+ return response.data;
1484
+ `);
1485
+
1486
+ expect(echoed.url).toBe(`${TELEGRAM_PATH}?chat_id=%40mychannel&text=hi`);
1487
+ } finally {
1488
+ await echoServer.close();
1489
+ }
1490
+ });
1491
+
1492
+ test("joins a relative token path onto baseURL", async () => {
1493
+ const echoServer: RunningLoopbackServer = await startEchoServer();
1494
+
1495
+ try {
1496
+ const echoed: EchoedRequest = await runAgainstLoopback(`
1497
+ const response = await axios.post(
1498
+ 'bot${BOT_TOKEN}/sendMessage',
1499
+ { chat_id: 1, text: 'hi' },
1500
+ { baseURL: 'http://127.0.0.1:${echoServer.port}' }
1501
+ );
1502
+ return response.data;
1503
+ `);
1504
+
1505
+ expect(echoed.method).toBe("POST");
1506
+ expect(echoed.url).toBe(TELEGRAM_PATH);
1507
+ } finally {
1508
+ await echoServer.close();
1509
+ }
1510
+ });
1511
+
1512
+ test("reaches api.telegram.org by name, dialling the address the guard approved", async () => {
1513
+ const echoServer: RunningLoopbackServer = await startEchoServer();
1514
+ const validationLookupSpy: ValidationLookupSpy = jest.spyOn(
1515
+ dns.promises,
1516
+ "lookup",
1517
+ ) as unknown as ValidationLookupSpy;
1518
+ validationLookupSpy.mockResolvedValue([
1519
+ { address: "127.0.0.1", family: 4 },
1520
+ ]);
1521
+
1522
+ try {
1523
+ const echoed: EchoedRequest = await runAgainstLoopback(`
1524
+ const response = await axios.post(
1525
+ 'http://api.telegram.org:${echoServer.port}${TELEGRAM_PATH}',
1526
+ { chat_id: '@mychannel', text: 'hi' }
1527
+ );
1528
+ return response.data;
1529
+ `);
1530
+
1531
+ expect(validationLookupSpy).toHaveBeenCalledWith("api.telegram.org", {
1532
+ all: true,
1533
+ });
1534
+ expect(echoed.host).toBe(`api.telegram.org:${echoServer.port}`);
1535
+ expect(echoed.url).toBe(TELEGRAM_PATH);
1536
+ } finally {
1537
+ await echoServer.close();
1538
+ }
1539
+ });
1540
+
1541
+ test.each(["/~user/hook", "/files/~backup/latest", "/urn:uuid:6e8bc430/x"])(
1542
+ "reaches the path %s",
1543
+ async (path: string) => {
1544
+ const echoServer: RunningLoopbackServer = await startEchoServer();
1545
+
1546
+ try {
1547
+ const echoed: EchoedRequest = await runAgainstLoopback(`
1548
+ const response = await axios.get('http://127.0.0.1:${echoServer.port}${path}');
1549
+ return response.data;
1550
+ `);
1551
+
1552
+ expect(echoed.url).toBe(path);
1553
+ } finally {
1554
+ await echoServer.close();
1555
+ }
1556
+ },
1557
+ );
1558
+
1559
+ test("a token path on an internal host is refused for its HOST, not as an invalid URL", async () => {
1560
+ delete process.env[PRIVATE_NETWORK_WEBHOOK_ALLOWLIST_ENV];
1561
+
1562
+ const message: string = await errorFromSandbox(
1563
+ `return await axios.post('http://169.254.169.254${TELEGRAM_PATH}', { a: 1 });`,
1564
+ );
1565
+
1566
+ expect(message).toMatch(REFUSAL);
1567
+ expect(message).not.toMatch(/not a valid URL/i);
1568
+ });
1569
+
1570
+ test("a token path on a public host clears the guard and fails only at transport", async () => {
1571
+ delete process.env[PRIVATE_NETWORK_WEBHOOK_ALLOWLIST_ENV];
1572
+
1573
+ const message: string = await errorFromSandbox(
1574
+ `try { return await axios.post('http://8.8.8.8${TELEGRAM_PATH}', {}, { timeout: 1 }); } catch (e) { return 'threw: ' + e.message; }`,
1575
+ );
1576
+
1577
+ expect(message).not.toMatch(REFUSAL);
1578
+ expect(message).not.toMatch(/not a valid URL/i);
1579
+ });
1580
+ });
@@ -59,4 +59,60 @@ describe("Route", () => {
59
59
  ])("preserves legitimate colon use in %s", (routeValue: string) => {
60
60
  expect(new Route(routeValue).toString()).toBe(routeValue);
61
61
  });
62
+
63
+ /*
64
+ * A Telegram token segment is scheme-like when it stands alone, so it is
65
+ * refused bare - but with its leading "/" it is an ordinary absolute path.
66
+ * URL.fromString relies on exactly this to keep Telegram URLs parseable.
67
+ */
68
+ test("accepts a token-like first segment once it has a leading slash", () => {
69
+ expect(() => {
70
+ return new Route("bot123456:ABC-def_ghi/sendMessage");
71
+ }).toThrowError(BadDataException);
72
+
73
+ expect(new Route("/bot123456:ABC-def_ghi/sendMessage").toString()).toBe(
74
+ "/bot123456:ABC-def_ghi/sendMessage",
75
+ );
76
+ });
77
+
78
+ test("addRoute prefixes the slash before validating a token-like string", () => {
79
+ expect(new Route("").addRoute("bot123456:ABC/sendMessage").toString()).toBe(
80
+ "/bot123456:ABC/sendMessage",
81
+ );
82
+ expect(new Route("/api").addRoute("bot1:x").toString()).toBe("/api/bot1:x");
83
+ });
84
+
85
+ test.each([
86
+ ["javascript:alert(1)", true],
87
+ ["JaVaScRiPt:alert(1)", true],
88
+ ["https://example.com", true],
89
+ ["bot123:ABC/sendMessage", true],
90
+ ["a.b+c-d:e", true],
91
+ ["/bot123:ABC/sendMessage", false],
92
+ ["api/:resourceId", false],
93
+ [":resourceId", false],
94
+ ["1:2/x", false],
95
+ ["_matrix/!room:server", false],
96
+ ["#frag:ment", false],
97
+ ["?next=a:b", false],
98
+ ["", false],
99
+ ])("hasSchemePrefix(%s) is %s", (routeValue: string, expected: boolean) => {
100
+ expect(Route.hasSchemePrefix(routeValue)).toBe(expected);
101
+ });
102
+
103
+ test.each(["/~user", "/files/~backup", "/a~b", "~user/hook", "/p?x=~y"])(
104
+ "accepts the unreserved character '~' in %s",
105
+ (routeValue: string) => {
106
+ expect(new Route(routeValue).toString()).toBe(routeValue);
107
+ },
108
+ );
109
+
110
+ test.each(["/a b", "/a|b", "/a`b", '/a"b', "/a<b", "/a>b", "/a\\b", "/a^b"])(
111
+ "still refuses the non-path character in %s",
112
+ (routeValue: string) => {
113
+ expect(() => {
114
+ return new Route(routeValue);
115
+ }).toThrowError(BadDataException);
116
+ },
117
+ );
62
118
  });