@oneuptime/common 12.0.28 → 12.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/Index.ts +11 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
- package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
- package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/OnCallCalendarAPI.ts +2194 -0
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/EnvironmentConfig.ts +138 -0
- package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/Index.ts +12 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
- package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
- package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Services/TeamMemberService.ts +489 -1
- package/Server/Services/TeamService.ts +50 -0
- package/Server/Services/UserNotificationSettingService.ts +44 -1
- package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
- package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
- package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
- package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
- package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
- package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
- package/Server/Utils/Response.ts +134 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
- package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
- package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
- package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
- package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
- package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
- package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/Helpers.ts +8 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
- package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
- package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
- package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
- package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
- package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
- package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
- package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
- package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
- package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
- package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
- package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
- package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Calendar/ICalendar.ts +442 -0
- package/Types/Date.ts +143 -13
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
- package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
- package/Types/OnCallDutyPolicy/Layer.ts +190 -35
- package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
- package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
- package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
- package/Types/Rum/SessionReplay.ts +62 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +11 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +102 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +12 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +396 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +43 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +97 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +9 -1
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Calendar/ICalendar.js +290 -0
- package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "crypto";
|
|
2
|
+
import logger from "../../../../Server/Utils/Logger";
|
|
3
|
+
import GoogleSecOpsClient, {
|
|
4
|
+
FetchAlertsResult,
|
|
5
|
+
FetchLike,
|
|
6
|
+
FetchResponseLike,
|
|
7
|
+
} from "../../../../Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient";
|
|
8
|
+
import APIException from "../../../../Types/Exception/ApiException";
|
|
9
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
10
|
+
import { getJestSpyOn } from "../../../Spy";
|
|
11
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* The ways a Google SecOps poll fails without the HTTP status saying so,
|
|
15
|
+
* and the ways it fails when the status does.
|
|
16
|
+
*
|
|
17
|
+
* legacyFetchAlertsView is a server-streaming method, and it reports a
|
|
18
|
+
* rejected query IN BAND: HTTP 200, with validSnapshotQuery /
|
|
19
|
+
* validBaselineQuery / queryValidationErrors / runtimeErrors carrying the
|
|
20
|
+
* complaint — and, because the terminal google.rpc.Status is appended as
|
|
21
|
+
* the stream's last element, sometimes with an error sitting behind
|
|
22
|
+
* perfectly good alert data. A parser that trusts response.ok reports
|
|
23
|
+
* every one of those as a healthy poll that found nothing, and the poller
|
|
24
|
+
* then advances its cursor past the window it never read.
|
|
25
|
+
*
|
|
26
|
+
* The other half of this file is the HTTP error contract an operator
|
|
27
|
+
* actually reads off `Last Error`: both body shapes (this host wraps its
|
|
28
|
+
* errors in the stream's array envelope, an error rejected at the edge
|
|
29
|
+
* does not), the ErrorInfo.reason branches that turn a status code into an
|
|
30
|
+
* instruction, and the 500-character echo that the integration doc and the
|
|
31
|
+
* connections page both quote verbatim.
|
|
32
|
+
*
|
|
33
|
+
* It closes with the exact 400 a customer hit in production. Google's
|
|
34
|
+
* authn runs BEFORE HTTP transcoding — a live probe returns 401, not that
|
|
35
|
+
* 400, for the same bad parameter without credentials — so the reported
|
|
36
|
+
* body is affirmative proof the service account worked and the request
|
|
37
|
+
* shape was ours. Anything that reads it as a credential problem sends the
|
|
38
|
+
* customer to regenerate a key that was never broken.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* A real RS256 key: the client genuinely signs a JWT assertion on the way
|
|
43
|
+
* to the token endpoint, and a fake key fails inside jwt.sign with an
|
|
44
|
+
* error that has nothing to do with what these tests are pinning.
|
|
45
|
+
*/
|
|
46
|
+
const { privateKey } = generateKeyPairSync("rsa", {
|
|
47
|
+
modulusLength: 2048,
|
|
48
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
49
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const GOOGLE_TOKEN_URI: string = "https://oauth2.googleapis.com/token";
|
|
53
|
+
|
|
54
|
+
const SERVICE_ACCOUNT_JSON: string = JSON.stringify({
|
|
55
|
+
client_email: "poller@example.iam.gserviceaccount.com",
|
|
56
|
+
private_key: privateKey,
|
|
57
|
+
token_uri: GOOGLE_TOKEN_URI,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const INSTANCE: string =
|
|
61
|
+
"projects/my-project/locations/us/instances/3f0a-instance";
|
|
62
|
+
|
|
63
|
+
const WINDOW_START: Date = new Date("2026-08-21T09:00:00.000Z");
|
|
64
|
+
const WINDOW_END: Date = new Date("2026-08-21T10:00:00.000Z");
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* The figure the integration doc and the connections page both print, and
|
|
68
|
+
* the width the lastError column is sized against.
|
|
69
|
+
*/
|
|
70
|
+
const BODY_ECHO_LIMIT: number = 500;
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* The HTTP-shaped opening the connections page's taxonomy splits on. An
|
|
74
|
+
* in-band rejection arrives on a 200 and must not wear it.
|
|
75
|
+
*/
|
|
76
|
+
const HTTP_FAILURE_PREFIX_PATTERN: RegExp = /failed \(HTTP \d+\)/;
|
|
77
|
+
|
|
78
|
+
// The scope the client asks for; the scope-insufficient hint must name it.
|
|
79
|
+
const CHRONICLE_SCOPE: string =
|
|
80
|
+
"https://www.googleapis.com/auth/cloud-platform";
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Verbatim, byte for byte, the body the customer's connection recorded.
|
|
84
|
+
* String.raw so the escaped quotes around "pageSize" survive into the
|
|
85
|
+
* fixture as escaped quotes rather than being unescaped by TypeScript —
|
|
86
|
+
* the point of the fixture is that it is the real wire bytes.
|
|
87
|
+
*/
|
|
88
|
+
const PRODUCTION_ERROR_BODY: string = String.raw`[{"error":{"code":400,"message":"Invalid JSON payload received. Unknown name \"pageSize\": Cannot bind query parameter. Field 'pageSize' could not be found in request message.","status":"INVALID_ARGUMENT","details":[{"@type":"type.googleapis.com/google.rpc.BadRequest","fieldViolations":[{"description":"Invalid JSON payload received. Unknown name \"pageSize\": Cannot bind query parameter."}]}]}}]`;
|
|
89
|
+
|
|
90
|
+
interface StubbedResponse {
|
|
91
|
+
status: number;
|
|
92
|
+
body: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface RecordedRequest {
|
|
96
|
+
url: string;
|
|
97
|
+
method: string;
|
|
98
|
+
headers: Record<string, string>;
|
|
99
|
+
body?: string | undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface CapturedLogs {
|
|
103
|
+
warnings: Array<string>;
|
|
104
|
+
errors: Array<string>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface AlertsFailure {
|
|
108
|
+
error: Error;
|
|
109
|
+
requests: Array<RecordedRequest>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* Routed by URL rather than by call order, because the 401 path re-mints a
|
|
114
|
+
* token mid-fetch and a positional recorder would hand the retry the wrong
|
|
115
|
+
* fixture.
|
|
116
|
+
*/
|
|
117
|
+
function makeFetch(responses: {
|
|
118
|
+
token: StubbedResponse;
|
|
119
|
+
alerts: StubbedResponse;
|
|
120
|
+
}): {
|
|
121
|
+
fetchImplementation: FetchLike;
|
|
122
|
+
requests: Array<RecordedRequest>;
|
|
123
|
+
} {
|
|
124
|
+
const requests: Array<RecordedRequest> = [];
|
|
125
|
+
|
|
126
|
+
const fetchImplementation: FetchLike = (
|
|
127
|
+
url: string,
|
|
128
|
+
init: {
|
|
129
|
+
method: string;
|
|
130
|
+
headers: Record<string, string>;
|
|
131
|
+
body?: string | undefined;
|
|
132
|
+
},
|
|
133
|
+
): Promise<FetchResponseLike> => {
|
|
134
|
+
requests.push({
|
|
135
|
+
url: url,
|
|
136
|
+
method: init.method,
|
|
137
|
+
headers: init.headers,
|
|
138
|
+
body: init.body,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const response: StubbedResponse =
|
|
142
|
+
url === GOOGLE_TOKEN_URI ? responses.token : responses.alerts;
|
|
143
|
+
|
|
144
|
+
return Promise.resolve({
|
|
145
|
+
ok: response.status >= 200 && response.status < 300,
|
|
146
|
+
status: response.status,
|
|
147
|
+
text: (): Promise<string> => {
|
|
148
|
+
return Promise.resolve(response.body);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return { fetchImplementation: fetchImplementation, requests: requests };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function makeClient(responses: {
|
|
157
|
+
token: StubbedResponse;
|
|
158
|
+
alerts: StubbedResponse;
|
|
159
|
+
}): { client: GoogleSecOpsClient; requests: Array<RecordedRequest> } {
|
|
160
|
+
const { fetchImplementation, requests } = makeFetch(responses);
|
|
161
|
+
|
|
162
|
+
const client: GoogleSecOpsClient = new GoogleSecOpsClient({
|
|
163
|
+
region: "us",
|
|
164
|
+
instanceResourceName: INSTANCE,
|
|
165
|
+
serviceAccountJson: SERVICE_ACCOUNT_JSON,
|
|
166
|
+
fetchImplementation: fetchImplementation,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return { client: client, requests: requests };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function okTokenResponse(): StubbedResponse {
|
|
173
|
+
return {
|
|
174
|
+
status: 200,
|
|
175
|
+
body: JSON.stringify({ access_token: "test-token", expires_in: 3600 }),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function alertsRequestsIn(
|
|
180
|
+
requests: Array<RecordedRequest>,
|
|
181
|
+
): Array<RecordedRequest> {
|
|
182
|
+
return requests.filter((request: RecordedRequest): boolean => {
|
|
183
|
+
return request.url !== GOOGLE_TOKEN_URI;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function tokenRequestsIn(
|
|
188
|
+
requests: Array<RecordedRequest>,
|
|
189
|
+
): Array<RecordedRequest> {
|
|
190
|
+
return requests.filter((request: RecordedRequest): boolean => {
|
|
191
|
+
return request.url === GOOGLE_TOKEN_URI;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Drive the real client to failure and hand back what an operator would read.
|
|
196
|
+
async function alertsFailure(alerts: StubbedResponse): Promise<AlertsFailure> {
|
|
197
|
+
const { client, requests } = makeClient({
|
|
198
|
+
token: okTokenResponse(),
|
|
199
|
+
alerts: alerts,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await client.fetchDetectionAlerts({
|
|
204
|
+
startTime: WINDOW_START,
|
|
205
|
+
endTime: WINDOW_END,
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return { error: error as Error, requests: requests };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw new Error(
|
|
212
|
+
"GoogleSecOpsClient was expected to reject this alerts response.",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseFailure(
|
|
217
|
+
bodyText: string,
|
|
218
|
+
maxReturnedAlerts?: number | undefined,
|
|
219
|
+
): Error {
|
|
220
|
+
try {
|
|
221
|
+
GoogleSecOpsClient.parseAlertsBody(bodyText, maxReturnedAlerts);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
return error as Error;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw new Error(
|
|
227
|
+
"GoogleSecOpsClient.parseAlertsBody was expected to throw for this body.",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/*
|
|
232
|
+
* Fresh arrays per call, so no test can observe another test's logs; the
|
|
233
|
+
* afterEach restore puts the real logger back either way.
|
|
234
|
+
*/
|
|
235
|
+
function captureLogs(): CapturedLogs {
|
|
236
|
+
const warnings: Array<string> = [];
|
|
237
|
+
const errors: Array<string> = [];
|
|
238
|
+
|
|
239
|
+
getJestSpyOn(logger, "warn").mockImplementation(((message: unknown): void => {
|
|
240
|
+
warnings.push(String(message));
|
|
241
|
+
}) as never);
|
|
242
|
+
|
|
243
|
+
getJestSpyOn(logger, "error").mockImplementation(((
|
|
244
|
+
message: unknown,
|
|
245
|
+
): void => {
|
|
246
|
+
errors.push(String(message));
|
|
247
|
+
}) as never);
|
|
248
|
+
|
|
249
|
+
return { warnings: warnings, errors: errors };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function errorInfoDetail(
|
|
253
|
+
reason: string,
|
|
254
|
+
metadata?: JSONObject | undefined,
|
|
255
|
+
): JSONObject {
|
|
256
|
+
const detail: JSONObject = {
|
|
257
|
+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
|
|
258
|
+
reason: reason,
|
|
259
|
+
domain: "chronicle.googleapis.com",
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (metadata) {
|
|
263
|
+
detail["metadata"] = metadata;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return detail;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function badRequestDetail(description: string): JSONObject {
|
|
270
|
+
return {
|
|
271
|
+
"@type": "type.googleapis.com/google.rpc.BadRequest",
|
|
272
|
+
fieldViolations: [{ description: description }],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function googleError(data: {
|
|
277
|
+
code: number;
|
|
278
|
+
status: string;
|
|
279
|
+
message: string;
|
|
280
|
+
details?: Array<JSONObject> | undefined;
|
|
281
|
+
}): JSONObject {
|
|
282
|
+
const error: JSONObject = {
|
|
283
|
+
code: data.code,
|
|
284
|
+
message: data.message,
|
|
285
|
+
status: data.status,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (data.details) {
|
|
289
|
+
error["details"] = data.details;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return error;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// The two wire shapes: the edge answers bare, the streaming method wraps.
|
|
296
|
+
function bareErrorBody(error: JSONObject): string {
|
|
297
|
+
return JSON.stringify({ error: error });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function streamErrorBody(error: JSONObject): string {
|
|
301
|
+
return JSON.stringify([{ error: error }]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function hintForErrorInfo(data: {
|
|
305
|
+
httpStatus: number;
|
|
306
|
+
rpcStatus: string;
|
|
307
|
+
reason: string;
|
|
308
|
+
metadata?: JSONObject | undefined;
|
|
309
|
+
}): string {
|
|
310
|
+
return GoogleSecOpsClient.describeHttpFailure(
|
|
311
|
+
data.httpStatus,
|
|
312
|
+
streamErrorBody(
|
|
313
|
+
googleError({
|
|
314
|
+
code: data.httpStatus,
|
|
315
|
+
status: data.rpcStatus,
|
|
316
|
+
message: `Request rejected: ${data.reason}.`,
|
|
317
|
+
details: [errorInfoDetail(data.reason, data.metadata)],
|
|
318
|
+
}),
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const IAM_RESOURCE: string =
|
|
324
|
+
"projects/my-project/locations/us/instances/3f0a-instance";
|
|
325
|
+
const IAM_PERMISSION: string = "chronicle.legacies.legacyFetchAlertsView";
|
|
326
|
+
|
|
327
|
+
afterEach(() => {
|
|
328
|
+
jest.restoreAllMocks();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("GoogleSecOpsClient in-band validation errors on an HTTP 200", () => {
|
|
332
|
+
test("validSnapshotQuery:false throws and carries the query parse error Chronicle reported", () => {
|
|
333
|
+
const parseError: string =
|
|
334
|
+
"line 1:12 mismatched input 'AND' expecting {'(', VALUE}";
|
|
335
|
+
|
|
336
|
+
const thrown: Error = parseFailure(
|
|
337
|
+
JSON.stringify([
|
|
338
|
+
{
|
|
339
|
+
validSnapshotQuery: false,
|
|
340
|
+
queryValidationErrors: [{ errorText: parseError }],
|
|
341
|
+
complete: true,
|
|
342
|
+
},
|
|
343
|
+
]),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(thrown).toBeInstanceOf(APIException);
|
|
347
|
+
expect(thrown.message).toContain("validSnapshotQuery=false");
|
|
348
|
+
expect(thrown.message).toContain(parseError);
|
|
349
|
+
|
|
350
|
+
/*
|
|
351
|
+
* The failure taxonomy the connections page is written against splits
|
|
352
|
+
* on the HTTP-shaped prefixes. This one carries no HTTP status because
|
|
353
|
+
* there was no HTTP failure, and must not be dressed up as one.
|
|
354
|
+
*/
|
|
355
|
+
expect(HTTP_FAILURE_PREFIX_PATTERN.test(thrown.message)).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("a validation entry with no known text field is rendered as JSON, never as [object Object]", () => {
|
|
359
|
+
const thrown: Error = parseFailure(
|
|
360
|
+
JSON.stringify([
|
|
361
|
+
{
|
|
362
|
+
validSnapshotQuery: false,
|
|
363
|
+
queryValidationErrors: [{ code: 3, detail: "unparseable predicate" }],
|
|
364
|
+
complete: true,
|
|
365
|
+
},
|
|
366
|
+
]),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(thrown.message).toContain("unparseable predicate");
|
|
370
|
+
expect(thrown.message).toContain("detail");
|
|
371
|
+
expect(thrown.message).not.toContain("[object Object]");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("validBaselineQuery:false throws on its own and is not conflated with the snapshot query", () => {
|
|
375
|
+
const thrown: Error = parseFailure(
|
|
376
|
+
JSON.stringify([{ validBaselineQuery: false, complete: true }]),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(thrown).toBeInstanceOf(APIException);
|
|
380
|
+
expect(thrown.message).toContain("validBaselineQuery=false");
|
|
381
|
+
expect(thrown.message).not.toContain("validSnapshotQuery=false");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("non-empty runtimeErrors throw, deduped across chunks, and the alerts ahead of them are not returned", async () => {
|
|
385
|
+
const runtimeError: string =
|
|
386
|
+
"Rule 'ur_ttp_brute_force' failed at runtime: field udm.principal.ip not found";
|
|
387
|
+
|
|
388
|
+
/*
|
|
389
|
+
* The same error restated in a later chunk, because a cumulative chunk
|
|
390
|
+
* repeats the whole list — concatenating would print it twice.
|
|
391
|
+
*/
|
|
392
|
+
const body: string = JSON.stringify([
|
|
393
|
+
{
|
|
394
|
+
alerts: { alerts: [{ id: "a-1" }] },
|
|
395
|
+
runtimeErrors: [runtimeError],
|
|
396
|
+
},
|
|
397
|
+
{ runtimeErrors: [runtimeError], complete: true },
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
const thrown: Error = parseFailure(body);
|
|
401
|
+
|
|
402
|
+
expect(thrown).toBeInstanceOf(APIException);
|
|
403
|
+
expect(thrown.message.split(runtimeError).length - 1).toBe(1);
|
|
404
|
+
|
|
405
|
+
// Through the real transport too: a 200 that carries this never resolves.
|
|
406
|
+
const failure: AlertsFailure = await alertsFailure({
|
|
407
|
+
status: 200,
|
|
408
|
+
body: body,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(failure.error).toBeInstanceOf(APIException);
|
|
412
|
+
expect(failure.error.message).toContain(runtimeError);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("empty error arrays and true validity flags are a healthy response, not a rejection", () => {
|
|
416
|
+
const result: FetchAlertsResult = GoogleSecOpsClient.parseAlertsBody(
|
|
417
|
+
JSON.stringify([
|
|
418
|
+
{
|
|
419
|
+
alerts: { alerts: [{ id: "a-1" }] },
|
|
420
|
+
validSnapshotQuery: true,
|
|
421
|
+
validBaselineQuery: true,
|
|
422
|
+
queryValidationErrors: [],
|
|
423
|
+
runtimeErrors: [],
|
|
424
|
+
complete: true,
|
|
425
|
+
progress: 1,
|
|
426
|
+
},
|
|
427
|
+
]),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
expect(result.alerts).toEqual([{ id: "a-1" }]);
|
|
431
|
+
expect(result.complete).toBe(true);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe("GoogleSecOpsClient truncation, completeness and the C3 self-check", () => {
|
|
436
|
+
test("tooManyAlerts warns and sets truncatedByCount without throwing away the alerts it did return", () => {
|
|
437
|
+
const logs: CapturedLogs = captureLogs();
|
|
438
|
+
|
|
439
|
+
const result: FetchAlertsResult = GoogleSecOpsClient.parseAlertsBody(
|
|
440
|
+
JSON.stringify([
|
|
441
|
+
{
|
|
442
|
+
alerts: { alerts: [{ id: "a-1" }, { id: "a-2" }] },
|
|
443
|
+
tooManyAlerts: true,
|
|
444
|
+
// proto3 renders int64 as a JSON string; the counts arrive quoted.
|
|
445
|
+
baselineAlertsCount: "12000",
|
|
446
|
+
filteredAlertsCount: "12000",
|
|
447
|
+
complete: true,
|
|
448
|
+
progress: 1,
|
|
449
|
+
},
|
|
450
|
+
]),
|
|
451
|
+
2,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
expect(result.alerts).toEqual([{ id: "a-1" }, { id: "a-2" }]);
|
|
455
|
+
expect(result.truncatedByCount).toBe(true);
|
|
456
|
+
expect(result.truncatedByBytes).toBe(false);
|
|
457
|
+
expect(result.complete).toBe(true);
|
|
458
|
+
expect(result.baselineAlertsCount).toBe(12000);
|
|
459
|
+
expect(result.filteredAlertsCount).toBe(12000);
|
|
460
|
+
expect(result.chunkCount).toBe(1);
|
|
461
|
+
|
|
462
|
+
expect(logs.warnings).toHaveLength(1);
|
|
463
|
+
expect(logs.warnings[0]!).toContain("tooManyAlerts");
|
|
464
|
+
expect(logs.errors).toEqual([]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("memoryLimitExceeded warns and sets truncatedByBytes without throwing", () => {
|
|
468
|
+
const logs: CapturedLogs = captureLogs();
|
|
469
|
+
|
|
470
|
+
const result: FetchAlertsResult = GoogleSecOpsClient.parseAlertsBody(
|
|
471
|
+
JSON.stringify([
|
|
472
|
+
{
|
|
473
|
+
alerts: { alerts: [{ id: "a-1" }] },
|
|
474
|
+
memoryLimitExceeded: true,
|
|
475
|
+
complete: true,
|
|
476
|
+
progress: 1,
|
|
477
|
+
},
|
|
478
|
+
]),
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
expect(result.alerts).toHaveLength(1);
|
|
482
|
+
expect(result.truncatedByBytes).toBe(true);
|
|
483
|
+
expect(result.truncatedByCount).toBe(false);
|
|
484
|
+
|
|
485
|
+
expect(logs.warnings).toHaveLength(1);
|
|
486
|
+
expect(logs.warnings[0]!).toContain("memoryLimitExceeded");
|
|
487
|
+
expect(logs.errors).toEqual([]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("a stream that ends without complete:true warns and reports complete:false rather than throwing", () => {
|
|
491
|
+
const logs: CapturedLogs = captureLogs();
|
|
492
|
+
|
|
493
|
+
const result: FetchAlertsResult = GoogleSecOpsClient.parseAlertsBody(
|
|
494
|
+
JSON.stringify([{ alerts: { alerts: [{ id: "a-1" }] }, progress: 0.5 }]),
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
expect(result.complete).toBe(false);
|
|
498
|
+
expect(result.progress).toBe(0.5);
|
|
499
|
+
expect(result.alerts).toHaveLength(1);
|
|
500
|
+
|
|
501
|
+
expect(logs.warnings).toHaveLength(1);
|
|
502
|
+
expect(logs.warnings[0]!).toContain("complete=true");
|
|
503
|
+
expect(logs.errors).toEqual([]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("a stream that does end with complete:true says nothing at all", () => {
|
|
507
|
+
const logs: CapturedLogs = captureLogs();
|
|
508
|
+
|
|
509
|
+
const result: FetchAlertsResult = GoogleSecOpsClient.parseAlertsBody(
|
|
510
|
+
JSON.stringify([
|
|
511
|
+
{ alerts: { alerts: [{ id: "a-1" }] }, complete: true, progress: 1 },
|
|
512
|
+
]),
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
expect(result.complete).toBe(true);
|
|
516
|
+
expect(logs.warnings).toEqual([]);
|
|
517
|
+
expect(logs.errors).toEqual([]);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
/*
|
|
521
|
+
* C3: no Google page says whether chunk.alerts is cumulative or
|
|
522
|
+
* incremental. The union is correct either way, so the only way the
|
|
523
|
+
* accumulated total can exceed the ceiling we asked for is if the dedupe
|
|
524
|
+
* key stopped identifying restated alerts — which is an answer to the
|
|
525
|
+
* doc question, logged where production can see it.
|
|
526
|
+
*/
|
|
527
|
+
test("accumulating past maxReturnedAlerts logs an error naming both counts, and stops exactly at the ceiling", () => {
|
|
528
|
+
const body: string = JSON.stringify([
|
|
529
|
+
{ alerts: { alerts: [{ id: "a-1" }, { id: "a-2" }] } },
|
|
530
|
+
{
|
|
531
|
+
alerts: { alerts: [{ id: "a-1" }, { id: "a-2" }, { id: "a-3" }] },
|
|
532
|
+
complete: true,
|
|
533
|
+
progress: 1,
|
|
534
|
+
},
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
const atCeiling: CapturedLogs = captureLogs();
|
|
538
|
+
const withinCeiling: FetchAlertsResult = GoogleSecOpsClient.parseAlertsBody(
|
|
539
|
+
body,
|
|
540
|
+
3,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
expect(withinCeiling.alerts).toHaveLength(3);
|
|
544
|
+
expect(atCeiling.errors).toEqual([]);
|
|
545
|
+
expect(atCeiling.warnings).toEqual([]);
|
|
546
|
+
|
|
547
|
+
const overCeiling: CapturedLogs = captureLogs();
|
|
548
|
+
const overrun: FetchAlertsResult = GoogleSecOpsClient.parseAlertsBody(
|
|
549
|
+
body,
|
|
550
|
+
2,
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// The overrun is reported, never thrown: the alerts themselves are real.
|
|
554
|
+
expect(overrun.alerts).toHaveLength(3);
|
|
555
|
+
expect(overCeiling.errors).toHaveLength(1);
|
|
556
|
+
expect(overCeiling.errors[0]!).toContain(String(overrun.alerts.length));
|
|
557
|
+
expect(overCeiling.errors[0]!).toContain("2");
|
|
558
|
+
expect(overCeiling.warnings).toEqual([]);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
describe("GoogleSecOpsClient errors appended to a 200 stream", () => {
|
|
563
|
+
const streamError: JSONObject = googleError({
|
|
564
|
+
code: 13,
|
|
565
|
+
status: "INTERNAL",
|
|
566
|
+
message: "Backend rule engine terminated the stream.",
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const goodChunks: Array<JSONObject> = [
|
|
570
|
+
{ alerts: { alerts: [{ id: "a-1" }] } },
|
|
571
|
+
{ alerts: { alerts: [{ id: "a-2" }] }, complete: true, progress: 1 },
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
test("the same chunks without a terminal error resolve, so the control is real", async () => {
|
|
575
|
+
const { client } = makeClient({
|
|
576
|
+
token: okTokenResponse(),
|
|
577
|
+
alerts: { status: 200, body: JSON.stringify(goodChunks) },
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const result: FetchAlertsResult = await client.fetchDetectionAlerts({
|
|
581
|
+
startTime: WINDOW_START,
|
|
582
|
+
endTime: WINDOW_END,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
expect(result.alerts).toEqual([{ id: "a-1" }, { id: "a-2" }]);
|
|
586
|
+
expect(result.chunkCount).toBe(2);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("a google.rpc.Status appended after good alert data throws instead of being masked by it", async () => {
|
|
590
|
+
const body: string = JSON.stringify([
|
|
591
|
+
...goodChunks,
|
|
592
|
+
{ error: streamError },
|
|
593
|
+
]);
|
|
594
|
+
|
|
595
|
+
const thrown: Error = parseFailure(body);
|
|
596
|
+
|
|
597
|
+
expect(thrown).toBeInstanceOf(APIException);
|
|
598
|
+
expect(thrown.message).toContain("13");
|
|
599
|
+
expect(thrown.message).toContain("INTERNAL");
|
|
600
|
+
expect(thrown.message).toContain(
|
|
601
|
+
"Backend rule engine terminated the stream.",
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
const failure: AlertsFailure = await alertsFailure({
|
|
605
|
+
status: 200,
|
|
606
|
+
body: body,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
expect(failure.error).toBeInstanceOf(APIException);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("the scan reaches an error at any index, not just the first element", () => {
|
|
613
|
+
// Index 1 of 2, behind a chunk that carries no alerts at all.
|
|
614
|
+
const thrown: Error = parseFailure(
|
|
615
|
+
JSON.stringify([{ progress: 0.25 }, { error: streamError }]),
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
expect(thrown).toBeInstanceOf(APIException);
|
|
619
|
+
expect(thrown.message).toContain(
|
|
620
|
+
"Backend rule engine terminated the stream.",
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
describe("GoogleSecOpsClient.describeHttpFailure", () => {
|
|
626
|
+
test("reads the same error out of the array envelope and the bare object, and appends behind the echo", () => {
|
|
627
|
+
const unauthenticated: JSONObject = googleError({
|
|
628
|
+
code: 401,
|
|
629
|
+
status: "UNAUTHENTICATED",
|
|
630
|
+
message: "Request had invalid authentication credentials.",
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const bare: string = GoogleSecOpsClient.describeHttpFailure(
|
|
634
|
+
401,
|
|
635
|
+
bareErrorBody(unauthenticated),
|
|
636
|
+
);
|
|
637
|
+
const wrapped: string = GoogleSecOpsClient.describeHttpFailure(
|
|
638
|
+
401,
|
|
639
|
+
streamErrorBody(unauthenticated),
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
expect(bare).not.toBe("");
|
|
643
|
+
expect(bare).toBe(wrapped);
|
|
644
|
+
|
|
645
|
+
// The hint is only ever appended to the tail of the echoed body.
|
|
646
|
+
expect(bare.startsWith(" — ")).toBe(true);
|
|
647
|
+
|
|
648
|
+
const denied: JSONObject = googleError({
|
|
649
|
+
code: 403,
|
|
650
|
+
status: "PERMISSION_DENIED",
|
|
651
|
+
message: "Permission denied.",
|
|
652
|
+
details: [
|
|
653
|
+
errorInfoDetail("IAM_PERMISSION_DENIED", {
|
|
654
|
+
resource: IAM_RESOURCE,
|
|
655
|
+
permission: IAM_PERMISSION,
|
|
656
|
+
}),
|
|
657
|
+
],
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
expect(
|
|
661
|
+
GoogleSecOpsClient.describeHttpFailure(403, bareErrorBody(denied)),
|
|
662
|
+
).toBe(
|
|
663
|
+
GoogleSecOpsClient.describeHttpFailure(403, streamErrorBody(denied)),
|
|
664
|
+
);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("an error at a non-zero index is found, and the elements ahead of it change nothing", () => {
|
|
668
|
+
const denied: JSONObject = googleError({
|
|
669
|
+
code: 403,
|
|
670
|
+
status: "PERMISSION_DENIED",
|
|
671
|
+
message: "Permission denied.",
|
|
672
|
+
details: [
|
|
673
|
+
errorInfoDetail("IAM_PERMISSION_DENIED", {
|
|
674
|
+
resource: IAM_RESOURCE,
|
|
675
|
+
permission: IAM_PERMISSION,
|
|
676
|
+
}),
|
|
677
|
+
],
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const behindData: string = GoogleSecOpsClient.describeHttpFailure(
|
|
681
|
+
403,
|
|
682
|
+
JSON.stringify([{ progress: 0.5 }, { error: denied }]),
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
expect(behindData).toContain(IAM_RESOURCE);
|
|
686
|
+
expect(behindData).toBe(
|
|
687
|
+
GoogleSecOpsClient.describeHttpFailure(403, streamErrorBody(denied)),
|
|
688
|
+
);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test("IAM_PERMISSION_DENIED renders the resource and permission Google named, not a fixed pair", () => {
|
|
692
|
+
const reported: string = hintForErrorInfo({
|
|
693
|
+
httpStatus: 403,
|
|
694
|
+
rpcStatus: "PERMISSION_DENIED",
|
|
695
|
+
reason: "IAM_PERMISSION_DENIED",
|
|
696
|
+
metadata: { resource: IAM_RESOURCE, permission: IAM_PERMISSION },
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(reported).toContain(IAM_RESOURCE);
|
|
700
|
+
expect(reported).toContain(IAM_PERMISSION);
|
|
701
|
+
|
|
702
|
+
const otherTenant: string = hintForErrorInfo({
|
|
703
|
+
httpStatus: 403,
|
|
704
|
+
rpcStatus: "PERMISSION_DENIED",
|
|
705
|
+
reason: "IAM_PERMISSION_DENIED",
|
|
706
|
+
metadata: {
|
|
707
|
+
resource: "projects/other/locations/europe/instances/9c11-instance",
|
|
708
|
+
permission: "chronicle.legacies.legacyGetAlert",
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
expect(otherTenant).toContain(
|
|
713
|
+
"projects/other/locations/europe/instances/9c11-instance",
|
|
714
|
+
);
|
|
715
|
+
expect(otherTenant).toContain("chronicle.legacies.legacyGetAlert");
|
|
716
|
+
expect(otherTenant).not.toContain(IAM_RESOURCE);
|
|
717
|
+
|
|
718
|
+
// Metadata is optional on ErrorInfo; the hint must survive its absence.
|
|
719
|
+
const withoutMetadata: string = hintForErrorInfo({
|
|
720
|
+
httpStatus: 403,
|
|
721
|
+
rpcStatus: "PERMISSION_DENIED",
|
|
722
|
+
reason: "IAM_PERMISSION_DENIED",
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
expect(withoutMetadata).not.toBe("");
|
|
726
|
+
expect(withoutMetadata).not.toContain(IAM_RESOURCE);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test("ACCESS_TOKEN_SCOPE_INSUFFICIENT names the Chronicle scope and is distinct from the other credential reasons", () => {
|
|
730
|
+
const scopeInsufficient: string = hintForErrorInfo({
|
|
731
|
+
httpStatus: 403,
|
|
732
|
+
rpcStatus: "PERMISSION_DENIED",
|
|
733
|
+
reason: "ACCESS_TOKEN_SCOPE_INSUFFICIENT",
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
expect(scopeInsufficient).toContain(CHRONICLE_SCOPE);
|
|
737
|
+
|
|
738
|
+
const expired: string = hintForErrorInfo({
|
|
739
|
+
httpStatus: 401,
|
|
740
|
+
rpcStatus: "UNAUTHENTICATED",
|
|
741
|
+
reason: "ACCESS_TOKEN_EXPIRED",
|
|
742
|
+
});
|
|
743
|
+
const missing: string = hintForErrorInfo({
|
|
744
|
+
httpStatus: 401,
|
|
745
|
+
rpcStatus: "UNAUTHENTICATED",
|
|
746
|
+
reason: "CREDENTIALS_MISSING",
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
for (const hint of [scopeInsufficient, expired, missing]) {
|
|
750
|
+
expect(hint).not.toBe("");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/*
|
|
754
|
+
* Three reasons that all mean "the token is wrong" and all need a
|
|
755
|
+
* different fix — collapsing any two of them is the failure this
|
|
756
|
+
* assertion exists to catch.
|
|
757
|
+
*/
|
|
758
|
+
const distinct: Set<string> = new Set<string>([
|
|
759
|
+
scopeInsufficient,
|
|
760
|
+
expired,
|
|
761
|
+
missing,
|
|
762
|
+
]);
|
|
763
|
+
expect(distinct.size).toBe(3);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test("the reason beats the status, and an unrecognized reason is still surfaced verbatim", () => {
|
|
767
|
+
const statusOnly: string = GoogleSecOpsClient.describeHttpFailure(401, "");
|
|
768
|
+
const withReason: string = hintForErrorInfo({
|
|
769
|
+
httpStatus: 401,
|
|
770
|
+
rpcStatus: "UNAUTHENTICATED",
|
|
771
|
+
reason: "ACCESS_TOKEN_EXPIRED",
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
expect(statusOnly).not.toBe("");
|
|
775
|
+
expect(withReason).not.toBe(statusOnly);
|
|
776
|
+
|
|
777
|
+
const novelReason: string = hintForErrorInfo({
|
|
778
|
+
httpStatus: 403,
|
|
779
|
+
rpcStatus: "PERMISSION_DENIED",
|
|
780
|
+
reason: "CHRONICLE_TENANT_SUSPENDED",
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
expect(novelReason).toContain("CHRONICLE_TENANT_SUSPENDED");
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("429 is classified as RESOURCE_EXHAUSTED, and RATE_LIMIT_EXCEEDED names the quota Google reported", () => {
|
|
787
|
+
const statusOnly: string = GoogleSecOpsClient.describeHttpFailure(
|
|
788
|
+
429,
|
|
789
|
+
"Quota exceeded.",
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
expect(statusOnly).toContain("RESOURCE_EXHAUSTED");
|
|
793
|
+
|
|
794
|
+
const quotaMetric: string =
|
|
795
|
+
"chronicle.googleapis.com/legacy_fetch_alerts_requests";
|
|
796
|
+
const quotaLimit: string = "LegacyFetchAlertsRequestsPerUserPerHour";
|
|
797
|
+
|
|
798
|
+
const withQuota: string = hintForErrorInfo({
|
|
799
|
+
httpStatus: 429,
|
|
800
|
+
rpcStatus: "RESOURCE_EXHAUSTED",
|
|
801
|
+
reason: "RATE_LIMIT_EXCEEDED",
|
|
802
|
+
metadata: { quota_metric: quotaMetric, quota_limit: quotaLimit },
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
expect(withQuota).toContain(quotaMetric);
|
|
806
|
+
expect(withQuota).toContain(quotaLimit);
|
|
807
|
+
expect(withQuota).not.toBe(statusOnly);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("the three 400s are told apart: unknown field, missing required field, and an opaque rejection", () => {
|
|
811
|
+
const unknownField: string = GoogleSecOpsClient.describeHttpFailure(
|
|
812
|
+
400,
|
|
813
|
+
PRODUCTION_ERROR_BODY,
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const missingRequired: string = GoogleSecOpsClient.describeHttpFailure(
|
|
817
|
+
400,
|
|
818
|
+
streamErrorBody(
|
|
819
|
+
googleError({
|
|
820
|
+
code: 400,
|
|
821
|
+
status: "INVALID_ARGUMENT",
|
|
822
|
+
message:
|
|
823
|
+
"Invalid JSON payload received. Missing required field 'snapshotQuery'.",
|
|
824
|
+
details: [badRequestDetail("Required field is missing.")],
|
|
825
|
+
}),
|
|
826
|
+
),
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
const opaque: string = GoogleSecOpsClient.describeHttpFailure(
|
|
830
|
+
400,
|
|
831
|
+
"Bad Request",
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
for (const hint of [unknownField, missingRequired, opaque]) {
|
|
835
|
+
expect(hint).not.toBe("");
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/*
|
|
839
|
+
* All three are OneUptime bugs, but the fix differs: stop sending a
|
|
840
|
+
* field, start sending one, or go and read the transcoder's complaint.
|
|
841
|
+
*/
|
|
842
|
+
const distinct: Set<string> = new Set<string>([
|
|
843
|
+
unknownField,
|
|
844
|
+
missingRequired,
|
|
845
|
+
opaque,
|
|
846
|
+
]);
|
|
847
|
+
expect(distinct.size).toBe(3);
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
describe("GoogleSecOpsClient HTTP failures as the operator reads them", () => {
|
|
852
|
+
test("an array-wrapped 403 is echoed whole and carries the IAM guidance on its tail", async () => {
|
|
853
|
+
const denied: JSONObject = googleError({
|
|
854
|
+
code: 403,
|
|
855
|
+
status: "PERMISSION_DENIED",
|
|
856
|
+
message: "The caller does not have permission.",
|
|
857
|
+
details: [
|
|
858
|
+
errorInfoDetail("IAM_PERMISSION_DENIED", {
|
|
859
|
+
resource: IAM_RESOURCE,
|
|
860
|
+
permission: IAM_PERMISSION,
|
|
861
|
+
}),
|
|
862
|
+
],
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const body: string = streamErrorBody(denied);
|
|
866
|
+
const guidance: string = GoogleSecOpsClient.describeHttpFailure(403, body);
|
|
867
|
+
|
|
868
|
+
const failure: AlertsFailure = await alertsFailure({
|
|
869
|
+
status: 403,
|
|
870
|
+
body: body,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
expect(failure.error).toBeInstanceOf(APIException);
|
|
874
|
+
expect(failure.error.message).toContain("HTTP 403");
|
|
875
|
+
|
|
876
|
+
// The whole body, so error.code and error.status reach the operator.
|
|
877
|
+
expect(failure.error.message).toContain(body);
|
|
878
|
+
|
|
879
|
+
// ...and the guidance naming the exact grant to make, behind it.
|
|
880
|
+
expect(failure.error.message.endsWith(guidance)).toBe(true);
|
|
881
|
+
expect(guidance).toContain(IAM_PERMISSION);
|
|
882
|
+
expect(guidance).toContain(IAM_RESOURCE);
|
|
883
|
+
|
|
884
|
+
// A 403 is final: only the 401 branch re-mints a token and retries.
|
|
885
|
+
expect(alertsRequestsIn(failure.requests)).toHaveLength(1);
|
|
886
|
+
expect(tokenRequestsIn(failure.requests)).toHaveLength(1);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test("a bare 401 gets the same treatment as the wrapped shape, after one fresh-token retry", async () => {
|
|
890
|
+
const unauthenticated: JSONObject = googleError({
|
|
891
|
+
code: 401,
|
|
892
|
+
status: "UNAUTHENTICATED",
|
|
893
|
+
message:
|
|
894
|
+
"Request had invalid authentication credentials. Expected OAuth 2 access token.",
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const body: string = bareErrorBody(unauthenticated);
|
|
898
|
+
|
|
899
|
+
/*
|
|
900
|
+
* Derived from the ARRAY-WRAPPED spelling of the same error, so the
|
|
901
|
+
* end-to-end assertion below is the both-shapes claim proven through
|
|
902
|
+
* the real transport rather than restated against the same input.
|
|
903
|
+
*/
|
|
904
|
+
const guidance: string = GoogleSecOpsClient.describeHttpFailure(
|
|
905
|
+
401,
|
|
906
|
+
streamErrorBody(unauthenticated),
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
const failure: AlertsFailure = await alertsFailure({
|
|
910
|
+
status: 401,
|
|
911
|
+
body: body,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
expect(failure.error).toBeInstanceOf(APIException);
|
|
915
|
+
expect(failure.error.message).toContain("HTTP 401");
|
|
916
|
+
expect(failure.error.message).toContain(body);
|
|
917
|
+
|
|
918
|
+
expect(guidance).not.toBe("");
|
|
919
|
+
expect(failure.error.message.endsWith(guidance)).toBe(true);
|
|
920
|
+
|
|
921
|
+
// A token revoked mid-lifetime deserves exactly one retry, not a loop.
|
|
922
|
+
expect(alertsRequestsIn(failure.requests)).toHaveLength(2);
|
|
923
|
+
expect(tokenRequestsIn(failure.requests)).toHaveLength(2);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test("a 429 reaches the operator classified as RESOURCE_EXHAUSTED with a back-off instruction", async () => {
|
|
927
|
+
const body: string = streamErrorBody(
|
|
928
|
+
googleError({
|
|
929
|
+
code: 429,
|
|
930
|
+
status: "RESOURCE_EXHAUSTED",
|
|
931
|
+
message: "Quota exceeded for quota metric 'Legacy fetch alerts'.",
|
|
932
|
+
}),
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
const guidance: string = GoogleSecOpsClient.describeHttpFailure(429, body);
|
|
936
|
+
const failure: AlertsFailure = await alertsFailure({
|
|
937
|
+
status: 429,
|
|
938
|
+
body: body,
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
expect(failure.error.message).toContain("HTTP 429");
|
|
942
|
+
expect(failure.error.message).toContain(body);
|
|
943
|
+
expect(failure.error.message.endsWith(guidance)).toBe(true);
|
|
944
|
+
|
|
945
|
+
// The classification has to come from the guidance, not from the echo.
|
|
946
|
+
expect(guidance).toContain("RESOURCE_EXHAUSTED");
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test("a long body is echoed to exactly the first 500 characters and no further", async () => {
|
|
950
|
+
const head: string = `${"A".repeat(BODY_ECHO_LIMIT - 1)}Z`;
|
|
951
|
+
const tailMarker: string = "TAIL-BEYOND-THE-ECHO-LIMIT";
|
|
952
|
+
|
|
953
|
+
const failure: AlertsFailure = await alertsFailure({
|
|
954
|
+
status: 500,
|
|
955
|
+
body: `${head}${tailMarker}${"B".repeat(600)}`,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
/*
|
|
959
|
+
* The echo is the tail of the message for a 500 (no guidance branch
|
|
960
|
+
* applies), so slicing from the body's first byte isolates it exactly.
|
|
961
|
+
*/
|
|
962
|
+
const echoed: string = failure.error.message.slice(
|
|
963
|
+
failure.error.message.indexOf("A"),
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
expect(echoed).toHaveLength(BODY_ECHO_LIMIT);
|
|
967
|
+
expect(echoed).toBe(head);
|
|
968
|
+
expect(failure.error.message).not.toContain(tailMarker);
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
describe("the pageSize 400 reported from production", () => {
|
|
973
|
+
test("the reported body is a transcoder error: BadRequest details and no ErrorInfo", () => {
|
|
974
|
+
const parsed: Array<JSONObject> = JSON.parse(
|
|
975
|
+
PRODUCTION_ERROR_BODY,
|
|
976
|
+
) as Array<JSONObject>;
|
|
977
|
+
|
|
978
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
979
|
+
|
|
980
|
+
const error: JSONObject = parsed[0]!["error"] as JSONObject;
|
|
981
|
+
expect(error["code"]).toBe(400);
|
|
982
|
+
expect(error["status"]).toBe("INVALID_ARGUMENT");
|
|
983
|
+
|
|
984
|
+
const detailTypes: Array<string> = (
|
|
985
|
+
error["details"] as Array<JSONObject>
|
|
986
|
+
).map((detail: JSONObject): string => {
|
|
987
|
+
return String(detail["@type"]);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
expect(detailTypes).toEqual(["type.googleapis.com/google.rpc.BadRequest"]);
|
|
991
|
+
|
|
992
|
+
/*
|
|
993
|
+
* AIP-193 requires a service-generated error to carry ErrorInfo. This
|
|
994
|
+
* one carries none, which is what proves it came from the HTTP
|
|
995
|
+
* transcoder rather than from Chronicle — and authn runs ahead of
|
|
996
|
+
* transcoding, so the customer's credentials had already been accepted.
|
|
997
|
+
*/
|
|
998
|
+
expect(
|
|
999
|
+
detailTypes.some((type: string): boolean => {
|
|
1000
|
+
return type.endsWith("google.rpc.ErrorInfo");
|
|
1001
|
+
}),
|
|
1002
|
+
).toBe(false);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
test("it is classified as a OneUptime request-shape bug, never as a credential or permission problem", () => {
|
|
1006
|
+
const hint: string = GoogleSecOpsClient.describeHttpFailure(
|
|
1007
|
+
400,
|
|
1008
|
+
PRODUCTION_ERROR_BODY,
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
expect(hint).not.toBe("");
|
|
1012
|
+
expect(hint).toMatch(/OneUptime/);
|
|
1013
|
+
expect(hint).toMatch(/not a credential or permission problem/i);
|
|
1014
|
+
|
|
1015
|
+
/*
|
|
1016
|
+
* The message an operator is sent to must not be any of the ones that
|
|
1017
|
+
* would have them regenerate a key or edit an IAM binding.
|
|
1018
|
+
*/
|
|
1019
|
+
const credentialGuidance: Array<string> = [
|
|
1020
|
+
GoogleSecOpsClient.describeHttpFailure(401, ""),
|
|
1021
|
+
GoogleSecOpsClient.describeHttpFailure(403, ""),
|
|
1022
|
+
hintForErrorInfo({
|
|
1023
|
+
httpStatus: 401,
|
|
1024
|
+
rpcStatus: "UNAUTHENTICATED",
|
|
1025
|
+
reason: "CREDENTIALS_MISSING",
|
|
1026
|
+
}),
|
|
1027
|
+
hintForErrorInfo({
|
|
1028
|
+
httpStatus: 401,
|
|
1029
|
+
rpcStatus: "UNAUTHENTICATED",
|
|
1030
|
+
reason: "ACCESS_TOKEN_EXPIRED",
|
|
1031
|
+
}),
|
|
1032
|
+
hintForErrorInfo({
|
|
1033
|
+
httpStatus: 403,
|
|
1034
|
+
rpcStatus: "PERMISSION_DENIED",
|
|
1035
|
+
reason: "ACCESS_TOKEN_SCOPE_INSUFFICIENT",
|
|
1036
|
+
}),
|
|
1037
|
+
hintForErrorInfo({
|
|
1038
|
+
httpStatus: 403,
|
|
1039
|
+
rpcStatus: "PERMISSION_DENIED",
|
|
1040
|
+
reason: "IAM_PERMISSION_DENIED",
|
|
1041
|
+
metadata: { resource: IAM_RESOURCE, permission: IAM_PERMISSION },
|
|
1042
|
+
}),
|
|
1043
|
+
];
|
|
1044
|
+
|
|
1045
|
+
for (const guidance of credentialGuidance) {
|
|
1046
|
+
expect(guidance).not.toBe("");
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
expect(credentialGuidance).not.toContain(hint);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
test("end to end the customer's body reaches Last Error verbatim with the actionable hint appended", async () => {
|
|
1053
|
+
const guidance: string = GoogleSecOpsClient.describeHttpFailure(
|
|
1054
|
+
400,
|
|
1055
|
+
PRODUCTION_ERROR_BODY,
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
const failure: AlertsFailure = await alertsFailure({
|
|
1059
|
+
status: 400,
|
|
1060
|
+
body: PRODUCTION_ERROR_BODY,
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
expect(failure.error).toBeInstanceOf(APIException);
|
|
1064
|
+
expect(failure.error.message).toContain("HTTP 400");
|
|
1065
|
+
|
|
1066
|
+
// 398 bytes, so the echo carries the whole thing under the 500 cap.
|
|
1067
|
+
expect(failure.error.message).toContain(PRODUCTION_ERROR_BODY);
|
|
1068
|
+
|
|
1069
|
+
expect(guidance).not.toBe("");
|
|
1070
|
+
expect(failure.error.message.endsWith(guidance)).toBe(true);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
test("the request the client now builds carries no pageSize, so this 400 cannot recur", async () => {
|
|
1074
|
+
const { client, requests } = makeClient({
|
|
1075
|
+
token: okTokenResponse(),
|
|
1076
|
+
alerts: {
|
|
1077
|
+
status: 200,
|
|
1078
|
+
body: JSON.stringify([
|
|
1079
|
+
{ alerts: { alerts: [{ id: "a-1" }] }, complete: true, progress: 1 },
|
|
1080
|
+
]),
|
|
1081
|
+
},
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
const result: FetchAlertsResult = await client.fetchDetectionAlerts({
|
|
1085
|
+
startTime: WINDOW_START,
|
|
1086
|
+
endTime: WINDOW_END,
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
expect(result.alerts).toHaveLength(1);
|
|
1090
|
+
|
|
1091
|
+
const params: URLSearchParams = new URL(alertsRequestsIn(requests)[0]!.url)
|
|
1092
|
+
.searchParams;
|
|
1093
|
+
|
|
1094
|
+
expect(params.has("pageSize")).toBe(false);
|
|
1095
|
+
|
|
1096
|
+
/*
|
|
1097
|
+
* The whole key set, not just the absent one: the bug shipped because
|
|
1098
|
+
* the existing assertions matched on the encoded ISO value, which
|
|
1099
|
+
* stays green no matter what the key around it is called.
|
|
1100
|
+
*/
|
|
1101
|
+
expect(Array.from(params.keys()).sort()).toEqual([
|
|
1102
|
+
"alertListOptions.maxReturnedAlerts",
|
|
1103
|
+
"timeRange.endTime",
|
|
1104
|
+
"timeRange.startTime",
|
|
1105
|
+
]);
|
|
1106
|
+
});
|
|
1107
|
+
});
|