@oneuptime/common 12.0.23 → 12.0.24
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/DetectionRule.ts +55 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
- package/Models/DatabaseModels/Project.ts +68 -0
- package/Models/DatabaseModels/User.ts +68 -0
- package/Server/API/TelemetryAPI.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/GzipRequestBody.ts +311 -0
- package/Server/Middleware/MultipartFormData.ts +87 -3
- package/Server/Services/DetectionRuleService.ts +73 -6
- package/Server/Services/Index.ts +205 -0
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
- package/Server/Services/NetworkDeviceService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
- package/Server/Services/ProjectService.ts +29 -19
- package/Server/Services/SecurityEventService.ts +90 -13
- package/Server/Services/StatusPageResourceService.ts +223 -0
- package/Server/Services/TelemetryAttributeService.ts +17 -0
- package/Server/Services/UserService.ts +4 -6
- package/Server/Types/Markdown.ts +161 -67
- package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
- package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
- package/Server/Utils/Attribution.ts +4 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
- package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
- package/Server/Utils/StartServer.ts +12 -32
- package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
- package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
- package/Server/Views/Partials/Head.ejs +2 -1
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
- package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
- package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
- package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
- package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
- package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
- package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
- package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
- package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
- package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
- package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
- package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
- package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
- package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
- package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
- package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
- package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
- package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
- package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
- package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
- package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
- package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
- package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
- package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
- package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
- package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
- package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
- package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
- package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
- package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
- package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
- package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
- package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
- package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
- package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
- package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
- package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
- package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
- package/Tests/UI/Components/SideOver.test.tsx +1 -1
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
- package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
- package/Tests/UI/Components/Toggle.test.tsx +25 -0
- package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
- package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
- package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
- package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
- package/Types/Code/YamlSyntax.ts +396 -0
- package/Types/Exception/ExceptionCode.ts +1 -0
- package/Types/Exception/PayloadTooLargeException.ts +8 -0
- package/Types/Marketing/Attribution.ts +10 -0
- package/Types/Marketing/MarketingEvent.ts +4 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
- package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
- package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
- package/Types/Telemetry/LlmMetricConventions.ts +124 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
- package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
- package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
- package/UI/Components/Detail/Detail.tsx +10 -0
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
- package/UI/Components/Forms/Fields/FormField.tsx +31 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
- package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
- package/UI/Components/Forms/Validation.ts +46 -0
- package/UI/Components/Input/Input.tsx +3 -0
- package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
- package/UI/Components/ModelTable/Column.ts +8 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
- package/UI/Components/SideOver/SideOver.tsx +5 -5
- package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
- package/UI/Components/StatusPage/ResourceList.tsx +197 -38
- package/UI/Components/Toggle/Toggle.tsx +9 -1
- package/UI/Components/Types/FieldType.ts +1 -0
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
- package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
- package/Utils/StatusPage/ResourceExplorer.ts +258 -0
- package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +76 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +76 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +17 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.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/GzipRequestBody.js +224 -0
- package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
- package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
- package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
- package/build/dist/Server/Services/DetectionRuleService.js +49 -4
- package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +204 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -45
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventService.js +57 -5
- package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
- package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +2 -12
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +147 -61
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -21
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Types/Code/YamlSyntax.js +212 -0
- package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
- package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
- package/build/dist/Types/Marketing/Attribution.js +10 -0
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
- package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +9 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +27 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
- package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Types/FieldType.js +1 -0
- package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/Tests/UI/Components/Toast.test.tsx +0 -197
- package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
- package/UI/Components/Toast/Toast.tsx +0 -189
- package/UI/Components/Toast/ToastInit.tsx +0 -110
- package/build/dist/UI/Components/Toast/Toast.js +0 -123
- package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
- package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import OnCallDutyPolicyEscalationRuleScheduleService from "../../../Server/Services/OnCallDutyPolicyEscalationRuleScheduleService";
|
|
2
|
+
import OnCallDutyPolicyScheduleService from "../../../Server/Services/OnCallDutyPolicyScheduleService";
|
|
3
|
+
import OnCallDutyPolicyFeedService from "../../../Server/Services/OnCallDutyPolicyFeedService";
|
|
4
|
+
import OnCallDutyPolicyTimeLogService from "../../../Server/Services/OnCallDutyPolicyTimeLogService";
|
|
5
|
+
import logger from "../../../Server/Utils/Logger";
|
|
6
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
7
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* https://github.com/OneUptime/oneuptime/issues/3411
|
|
11
|
+
*
|
|
12
|
+
* The persisted roster columns on OnCallDutyPolicySchedule (currentUserIdOnRoster
|
|
13
|
+
* and friends) are derived state, and one of their inputs is WHICH on-call
|
|
14
|
+
* policies escalate to the schedule: getSingleAttachedPolicyId resolves the
|
|
15
|
+
* roster in a policy's context only when exactly one policy is attached, which
|
|
16
|
+
* is what makes that policy's POLICY-SCOPED user overrides count.
|
|
17
|
+
*
|
|
18
|
+
* Attaching or detaching a schedule therefore changes the correct answer. Every
|
|
19
|
+
* other input - layers, layer users, overrides - already refreshes the roster on
|
|
20
|
+
* change; this one did not, so after attaching a schedule to its first policy
|
|
21
|
+
* the roster kept the pre-attach, policy-blind answer until the next natural
|
|
22
|
+
* hand-off. The schedule page's "X is currently on the roster" banner and the
|
|
23
|
+
* "On Call Now" column then named the originally-scheduled user for the whole
|
|
24
|
+
* override window while alert routing - which always resolves live, with the
|
|
25
|
+
* policy id - paged the substitute.
|
|
26
|
+
*
|
|
27
|
+
* The hooks are protected, so they are invoked through `as any`, the same way
|
|
28
|
+
* OnCallDutyPolicyUserOverrideEdit.test.ts does.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const PROJECT_ID: ObjectID = new ObjectID("project-1");
|
|
32
|
+
const SCHEDULE_ID: ObjectID = new ObjectID("schedule-1");
|
|
33
|
+
const OTHER_SCHEDULE_ID: ObjectID = new ObjectID("schedule-2");
|
|
34
|
+
const POLICY_ID: ObjectID = new ObjectID("policy-1");
|
|
35
|
+
const RULE_ID: ObjectID = new ObjectID("rule-1");
|
|
36
|
+
const LINK_ID: ObjectID = new ObjectID("link-1");
|
|
37
|
+
|
|
38
|
+
function silenceLoggerError(): void {
|
|
39
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
40
|
+
return undefined;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* Everything the hooks touch besides the roster refresh: feed items, time logs
|
|
46
|
+
* and the live roster lookup. Stubbed so each test asserts one thing - which
|
|
47
|
+
* schedules got their roster re-resolved.
|
|
48
|
+
*/
|
|
49
|
+
function stubCollaborators(options?: {
|
|
50
|
+
currentUserInSchedule?: ObjectID | null;
|
|
51
|
+
}): void {
|
|
52
|
+
jest
|
|
53
|
+
.spyOn(OnCallDutyPolicyScheduleService, "getCurrentUserIdInSchedule")
|
|
54
|
+
.mockResolvedValue(
|
|
55
|
+
options?.currentUserInSchedule === undefined
|
|
56
|
+
? null
|
|
57
|
+
: options.currentUserInSchedule,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
jest
|
|
61
|
+
.spyOn(OnCallDutyPolicyFeedService, "createOnCallDutyPolicyFeedItem")
|
|
62
|
+
.mockResolvedValue(undefined as never);
|
|
63
|
+
|
|
64
|
+
jest
|
|
65
|
+
.spyOn(OnCallDutyPolicyTimeLogService, "startTimeLogForUser")
|
|
66
|
+
.mockResolvedValue(undefined as never);
|
|
67
|
+
|
|
68
|
+
jest
|
|
69
|
+
.spyOn(OnCallDutyPolicyTimeLogService, "endTimeLogForUser")
|
|
70
|
+
.mockResolvedValue(undefined as never);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* Typed loosely on purpose: jest.SpiedFunction is invariant in its call
|
|
75
|
+
* signature, so the precise spy type does not widen to SpiedFunction<any> and
|
|
76
|
+
* every call site would need the exact generic restated.
|
|
77
|
+
*/
|
|
78
|
+
function spyOnRosterRefresh(): any {
|
|
79
|
+
return jest
|
|
80
|
+
.spyOn(
|
|
81
|
+
OnCallDutyPolicyScheduleService,
|
|
82
|
+
"refreshCurrentUserIdAndHandoffTimeInSchedule",
|
|
83
|
+
)
|
|
84
|
+
.mockResolvedValue({
|
|
85
|
+
currentUserId: null,
|
|
86
|
+
handOffTimeAt: null,
|
|
87
|
+
nextUserId: null,
|
|
88
|
+
nextHandOffTimeAt: null,
|
|
89
|
+
rosterStartAt: null,
|
|
90
|
+
nextRosterStartAt: null,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function refreshedScheduleIds(spy: any): Array<string> {
|
|
95
|
+
return spy.mock.calls.map((call: Array<unknown>): string => {
|
|
96
|
+
return (call[0] as ObjectID).toString();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function linkRow(scheduleId: ObjectID): any {
|
|
101
|
+
return {
|
|
102
|
+
id: LINK_ID,
|
|
103
|
+
projectId: PROJECT_ID,
|
|
104
|
+
onCallDutyPolicyScheduleId: scheduleId,
|
|
105
|
+
onCallDutyPolicySchedule: { name: "Primary schedule" },
|
|
106
|
+
onCallDutyPolicyEscalationRule: { name: "Rule 1", id: RULE_ID, order: 1 },
|
|
107
|
+
onCallDutyPolicy: { name: "Payments", id: POLICY_ID },
|
|
108
|
+
createdByUserId: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("Attaching / detaching a schedule re-resolves its persisted roster (issue #3411)", () => {
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
jest.restoreAllMocks();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("onCreateSuccess refreshes the attached schedule's roster", async () => {
|
|
118
|
+
silenceLoggerError();
|
|
119
|
+
stubCollaborators();
|
|
120
|
+
|
|
121
|
+
jest
|
|
122
|
+
.spyOn(OnCallDutyPolicyEscalationRuleScheduleService, "findOneById")
|
|
123
|
+
.mockResolvedValue(linkRow(SCHEDULE_ID));
|
|
124
|
+
|
|
125
|
+
const refresh: any = spyOnRosterRefresh();
|
|
126
|
+
|
|
127
|
+
await (
|
|
128
|
+
OnCallDutyPolicyEscalationRuleScheduleService as any
|
|
129
|
+
).onCreateSuccess({}, { id: LINK_ID });
|
|
130
|
+
|
|
131
|
+
expect(refreshedScheduleIds(refresh)).toEqual([SCHEDULE_ID.toString()]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* The refresh has to happen even when the schedule currently pages nobody:
|
|
136
|
+
* that is precisely the state where the roster is most misleading, and the
|
|
137
|
+
* notification/time-log code below the refresh returns early there. Placing
|
|
138
|
+
* the refresh after those early returns would have made this case a silent
|
|
139
|
+
* no-op.
|
|
140
|
+
*/
|
|
141
|
+
test("onCreateSuccess refreshes even when nobody is on call in the schedule", async () => {
|
|
142
|
+
silenceLoggerError();
|
|
143
|
+
stubCollaborators({ currentUserInSchedule: null });
|
|
144
|
+
|
|
145
|
+
jest
|
|
146
|
+
.spyOn(OnCallDutyPolicyEscalationRuleScheduleService, "findOneById")
|
|
147
|
+
.mockResolvedValue(linkRow(SCHEDULE_ID));
|
|
148
|
+
|
|
149
|
+
const refresh: any = spyOnRosterRefresh();
|
|
150
|
+
|
|
151
|
+
await (
|
|
152
|
+
OnCallDutyPolicyEscalationRuleScheduleService as any
|
|
153
|
+
).onCreateSuccess({}, { id: LINK_ID });
|
|
154
|
+
|
|
155
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a failing roster refresh does not fail the attach", async () => {
|
|
159
|
+
silenceLoggerError();
|
|
160
|
+
stubCollaborators();
|
|
161
|
+
|
|
162
|
+
jest
|
|
163
|
+
.spyOn(OnCallDutyPolicyEscalationRuleScheduleService, "findOneById")
|
|
164
|
+
.mockResolvedValue(linkRow(SCHEDULE_ID));
|
|
165
|
+
|
|
166
|
+
jest
|
|
167
|
+
.spyOn(
|
|
168
|
+
OnCallDutyPolicyScheduleService,
|
|
169
|
+
"refreshCurrentUserIdAndHandoffTimeInSchedule",
|
|
170
|
+
)
|
|
171
|
+
.mockRejectedValue(new Error("redis down"));
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
(OnCallDutyPolicyEscalationRuleScheduleService as any).onCreateSuccess(
|
|
175
|
+
{},
|
|
176
|
+
{ id: LINK_ID },
|
|
177
|
+
),
|
|
178
|
+
).resolves.toBeDefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("onDeleteSuccess refreshes the detached schedule's roster", async () => {
|
|
182
|
+
silenceLoggerError();
|
|
183
|
+
stubCollaborators();
|
|
184
|
+
|
|
185
|
+
const refresh: any = spyOnRosterRefresh();
|
|
186
|
+
|
|
187
|
+
await (
|
|
188
|
+
OnCallDutyPolicyEscalationRuleScheduleService as any
|
|
189
|
+
).onDeleteSuccess(
|
|
190
|
+
{ carryForward: { deletedItems: [linkRow(SCHEDULE_ID)] } },
|
|
191
|
+
[LINK_ID],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(refreshedScheduleIds(refresh)).toEqual([SCHEDULE_ID.toString()]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/*
|
|
198
|
+
* Removing one schedule from several escalation rules of the same policy is
|
|
199
|
+
* ONE context change. Refreshing per row would re-diff the same roster
|
|
200
|
+
* repeatedly, and each diff can emit a hand-off page - so a tidy-up that
|
|
201
|
+
* removes three rules must not ring the same phone three times.
|
|
202
|
+
*/
|
|
203
|
+
test("onDeleteSuccess refreshes each affected schedule exactly once", async () => {
|
|
204
|
+
silenceLoggerError();
|
|
205
|
+
stubCollaborators();
|
|
206
|
+
|
|
207
|
+
const refresh: any = spyOnRosterRefresh();
|
|
208
|
+
|
|
209
|
+
await (
|
|
210
|
+
OnCallDutyPolicyEscalationRuleScheduleService as any
|
|
211
|
+
).onDeleteSuccess(
|
|
212
|
+
{
|
|
213
|
+
carryForward: {
|
|
214
|
+
deletedItems: [
|
|
215
|
+
linkRow(SCHEDULE_ID),
|
|
216
|
+
linkRow(SCHEDULE_ID),
|
|
217
|
+
linkRow(OTHER_SCHEDULE_ID),
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
[LINK_ID],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(refreshedScheduleIds(refresh).sort()).toEqual(
|
|
225
|
+
[OTHER_SCHEDULE_ID.toString(), SCHEDULE_ID.toString()].sort(),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("onDeleteSuccess refreshes before the per-item early return for an uncovered schedule", async () => {
|
|
230
|
+
silenceLoggerError();
|
|
231
|
+
stubCollaborators({ currentUserInSchedule: null });
|
|
232
|
+
|
|
233
|
+
const refresh: any = spyOnRosterRefresh();
|
|
234
|
+
|
|
235
|
+
await (
|
|
236
|
+
OnCallDutyPolicyEscalationRuleScheduleService as any
|
|
237
|
+
).onDeleteSuccess(
|
|
238
|
+
{ carryForward: { deletedItems: [linkRow(SCHEDULE_ID)] } },
|
|
239
|
+
[LINK_ID],
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import SecurityEventService, {
|
|
2
|
+
DetectionMatchGroup,
|
|
3
|
+
FindDetectionMatchesData,
|
|
4
|
+
} from "../../../Server/Services/SecurityEventService";
|
|
5
|
+
import { Results } from "../../../Server/Services/AnalyticsDatabaseService";
|
|
6
|
+
import { getQuerySettings } from "../../../Server/Utils/AnalyticsDatabase/QuerySettingsHelper";
|
|
7
|
+
import {
|
|
8
|
+
SQL,
|
|
9
|
+
Statement,
|
|
10
|
+
} from "../../../Server/Utils/AnalyticsDatabase/Statement";
|
|
11
|
+
import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
|
|
12
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
13
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
14
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
15
|
+
import { getJestSpyOn } from "../../Spy";
|
|
16
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* The SQL contract of SecurityEventService.findDetectionMatches — the one
|
|
20
|
+
* query the detection engine runs per due rule (issue #3398 added the
|
|
21
|
+
* distinct-count semantics).
|
|
22
|
+
*
|
|
23
|
+
* Everything asserted here is invisible from the TypeScript call site and
|
|
24
|
+
* only observable as query text / bound params, so this file pins the
|
|
25
|
+
* rendered Statement:
|
|
26
|
+
*
|
|
27
|
+
* - a rule WITHOUT a distinct-count field selects a constant
|
|
28
|
+
* `0 AS distinctCount` (never an aggregate), gets NO HAVING clause at
|
|
29
|
+
* threshold 1, and orders by matchCount;
|
|
30
|
+
* - a raw-count threshold > 1 becomes `HAVING matchCount >= {bound}`;
|
|
31
|
+
* - a distinct-count rule counts `uniqExact(nullIf(<expr>, ''))` — empty
|
|
32
|
+
* values excluded — thresholds and orders on distinctCount, and gets
|
|
33
|
+
* the HAVING clause EVEN at threshold 1, because a group whose matches
|
|
34
|
+
* all lack the counted field has distinctCount 0 and must not fire;
|
|
35
|
+
* - the threshold is clamped to >= 1 before it is bound;
|
|
36
|
+
* - projectId / time-window predicates and the maxGroups LIMIT stay
|
|
37
|
+
* bound parameters, whatever the threshold shape;
|
|
38
|
+
* - result rows map distinctCount (default 0 when the column is absent)
|
|
39
|
+
* without disturbing the existing field mapping.
|
|
40
|
+
*
|
|
41
|
+
* Every service boundary is stubbed: executeQuery captures the Statement,
|
|
42
|
+
* and the database handle only supplies the schema name. No ClickHouse.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
type Spy = ReturnType<typeof getJestSpyOn>;
|
|
46
|
+
|
|
47
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
48
|
+
"22222222-2222-4222-8222-222222222222",
|
|
49
|
+
);
|
|
50
|
+
const START_TIME: Date = new Date("2026-08-25T10:00:00.000Z");
|
|
51
|
+
const END_TIME: Date = new Date("2026-08-25T10:05:00.000Z");
|
|
52
|
+
const DATABASE_NAME: string = "oneuptime";
|
|
53
|
+
const MAX_GROUPS: number = 50;
|
|
54
|
+
|
|
55
|
+
// The trailing SETTINGS clause is shared verbatim by every variant below.
|
|
56
|
+
const SETTINGS_SUFFIX: string = getQuerySettings({
|
|
57
|
+
maxExecutionTimeInSeconds: 60,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* Captures the Statement handed to executeQuery, returning canned rows.
|
|
62
|
+
* `rows: undefined` simulates a response body with no `data` key at all.
|
|
63
|
+
*/
|
|
64
|
+
function stubExecuteQuery(rows?: Array<JSONObject>): Spy {
|
|
65
|
+
getJestSpyOn(
|
|
66
|
+
SecurityEventService.database,
|
|
67
|
+
"getDatasourceOptions",
|
|
68
|
+
).mockReturnValue({ database: DATABASE_NAME } as never);
|
|
69
|
+
|
|
70
|
+
const fakeResult: Results = {
|
|
71
|
+
json: (): Promise<unknown> => {
|
|
72
|
+
return Promise.resolve(rows === undefined ? {} : { data: rows });
|
|
73
|
+
},
|
|
74
|
+
} as unknown as Results;
|
|
75
|
+
|
|
76
|
+
return getJestSpyOn(SecurityEventService, "executeQuery").mockResolvedValue(
|
|
77
|
+
fakeResult as never,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* Fragments the way SigmaClickhouseCompiler hands them over: every
|
|
83
|
+
* rule-controlled value is a bound parameter, field references are bound
|
|
84
|
+
* Identifiers.
|
|
85
|
+
*/
|
|
86
|
+
function buildWhereFragment(): Statement {
|
|
87
|
+
return SQL`statusName = ${{
|
|
88
|
+
type: TableColumnType.Text,
|
|
89
|
+
value: "Failure",
|
|
90
|
+
}}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildFieldExpression(field: string): Statement {
|
|
94
|
+
return SQL`${field}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function callFindDetectionMatches(
|
|
98
|
+
overrides: Partial<FindDetectionMatchesData> = {},
|
|
99
|
+
): Promise<Array<DetectionMatchGroup>> {
|
|
100
|
+
return SecurityEventService.findDetectionMatches({
|
|
101
|
+
projectId: PROJECT_ID,
|
|
102
|
+
startTime: START_TIME,
|
|
103
|
+
endTime: END_TIME,
|
|
104
|
+
whereFragment: buildWhereFragment(),
|
|
105
|
+
groupByExpression: null,
|
|
106
|
+
distinctCountExpression: null,
|
|
107
|
+
minMatchCount: 1,
|
|
108
|
+
maxGroups: MAX_GROUPS,
|
|
109
|
+
...overrides,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function capturedStatement(spy: Spy): Statement {
|
|
114
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
115
|
+
return spy.mock.calls[0]![0] as Statement;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe("SecurityEventService.findDetectionMatches — SQL contract", () => {
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
jest.restoreAllMocks();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("no distinct-count expression, threshold 1 (the pre-#3398 shape)", () => {
|
|
124
|
+
test("selects a constant 0 distinctCount, emits no HAVING, and orders by matchCount", async () => {
|
|
125
|
+
const spy: Spy = stubExecuteQuery([]);
|
|
126
|
+
|
|
127
|
+
await callFindDetectionMatches();
|
|
128
|
+
|
|
129
|
+
const statement: Statement = capturedStatement(spy);
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* Full-text equality: this is the exact query every existing rule
|
|
133
|
+
* (threshold 1, no distinct field) runs, so nothing about it may
|
|
134
|
+
* drift as a side effect of the new parameters.
|
|
135
|
+
*/
|
|
136
|
+
expect(statement.query).toBe(
|
|
137
|
+
"SELECT '' AS groupValue, count() AS matchCount, 0 AS distinctCount, " +
|
|
138
|
+
"any(message) AS sampleMessage, " +
|
|
139
|
+
"arrayDistinct(arrayFlatten(groupArray(20)(observables))) AS sampleObservables " +
|
|
140
|
+
`FROM ${DATABASE_NAME}.SecurityEventItemV1 ` +
|
|
141
|
+
"WHERE projectId = {p0:String} AND time >= {p1:DateTime64(9)} AND time < {p2:DateTime64(9)} " +
|
|
142
|
+
"AND (statusName = {p3:String}) " +
|
|
143
|
+
"GROUP BY groupValue ORDER BY matchCount DESC LIMIT {p4:Int32}" +
|
|
144
|
+
SETTINGS_SUFFIX,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(statement.query).not.toContain("HAVING");
|
|
148
|
+
expect(statement.query).not.toContain("uniqExact");
|
|
149
|
+
|
|
150
|
+
expect(statement.query_params).toStrictEqual({
|
|
151
|
+
p0: PROJECT_ID.toString(),
|
|
152
|
+
p1: OneUptimeDate.toClickhouseDateTime64(START_TIME),
|
|
153
|
+
p2: OneUptimeDate.toClickhouseDateTime64(END_TIME),
|
|
154
|
+
p3: "Failure",
|
|
155
|
+
p4: MAX_GROUPS,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("raw match-count threshold above 1", () => {
|
|
161
|
+
test("threshold 5 becomes HAVING matchCount >= a bound param and every other binding survives the renumbering", async () => {
|
|
162
|
+
const spy: Spy = stubExecuteQuery([]);
|
|
163
|
+
|
|
164
|
+
await callFindDetectionMatches({ minMatchCount: 5 });
|
|
165
|
+
|
|
166
|
+
const statement: Statement = capturedStatement(spy);
|
|
167
|
+
|
|
168
|
+
expect(statement.query).toContain(
|
|
169
|
+
"GROUP BY groupValue HAVING matchCount >= {p4:Int32} ORDER BY matchCount DESC LIMIT {p5:Int32}",
|
|
170
|
+
);
|
|
171
|
+
// The threshold thresholds the RAW count — never the constant 0.
|
|
172
|
+
expect(statement.query).not.toContain("distinctCount >=");
|
|
173
|
+
expect(statement.query).not.toContain("ORDER BY distinctCount");
|
|
174
|
+
|
|
175
|
+
/*
|
|
176
|
+
* HAVING is appended mid-statement, which renumbers the params after
|
|
177
|
+
* it — the tenant/time/limit bindings must survive that renumbering
|
|
178
|
+
* with their values intact.
|
|
179
|
+
*/
|
|
180
|
+
expect(statement.query).toContain(
|
|
181
|
+
"WHERE projectId = {p0:String} AND time >= {p1:DateTime64(9)} AND time < {p2:DateTime64(9)} AND (statusName = {p3:String})",
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(statement.query_params).toStrictEqual({
|
|
185
|
+
p0: PROJECT_ID.toString(),
|
|
186
|
+
p1: OneUptimeDate.toClickhouseDateTime64(START_TIME),
|
|
187
|
+
p2: OneUptimeDate.toClickhouseDateTime64(END_TIME),
|
|
188
|
+
p3: "Failure",
|
|
189
|
+
p4: 5,
|
|
190
|
+
p5: MAX_GROUPS,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("distinct-count expression", () => {
|
|
196
|
+
test("aggregates uniqExact(nullIf(expr, '')) and gets HAVING distinctCount EVEN at threshold 1", async () => {
|
|
197
|
+
const spy: Spy = stubExecuteQuery([]);
|
|
198
|
+
|
|
199
|
+
await callFindDetectionMatches({
|
|
200
|
+
distinctCountExpression: buildFieldExpression("targetUser"),
|
|
201
|
+
minMatchCount: 1,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const statement: Statement = capturedStatement(spy);
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
* nullIf(expr, '') keeps empty values out of the distinct count —
|
|
208
|
+
* "5 distinct usernames" must never count rows carrying no
|
|
209
|
+
* username. And the HAVING clause exists at threshold 1 because a
|
|
210
|
+
* group whose matches all lack the field has distinctCount 0.
|
|
211
|
+
*/
|
|
212
|
+
expect(statement.query).toContain(
|
|
213
|
+
"count() AS matchCount, uniqExact(nullIf({p0:Identifier}, '')) AS distinctCount",
|
|
214
|
+
);
|
|
215
|
+
expect(statement.query).toContain(
|
|
216
|
+
"GROUP BY groupValue HAVING distinctCount >= {p5:Int32} ORDER BY distinctCount DESC LIMIT {p6:Int32}",
|
|
217
|
+
);
|
|
218
|
+
expect(statement.query).not.toContain("matchCount >=");
|
|
219
|
+
|
|
220
|
+
expect(statement.query_params["p0"]).toBe("targetUser");
|
|
221
|
+
expect(statement.query_params["p5"]).toBe(1);
|
|
222
|
+
expect(statement.query_params["p6"]).toBe(MAX_GROUPS);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("group-by and distinct expressions bind as separate identifiers, threshold on distinctCount", async () => {
|
|
226
|
+
const spy: Spy = stubExecuteQuery([]);
|
|
227
|
+
|
|
228
|
+
await callFindDetectionMatches({
|
|
229
|
+
groupByExpression: buildFieldExpression("principalUser"),
|
|
230
|
+
distinctCountExpression: buildFieldExpression("targetUser"),
|
|
231
|
+
minMatchCount: 3,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const statement: Statement = capturedStatement(spy);
|
|
235
|
+
|
|
236
|
+
expect(statement.query).toContain(
|
|
237
|
+
"SELECT {p0:Identifier} AS groupValue, count() AS matchCount, uniqExact(nullIf({p1:Identifier}, '')) AS distinctCount",
|
|
238
|
+
);
|
|
239
|
+
expect(statement.query).toContain(
|
|
240
|
+
"HAVING distinctCount >= {p6:Int32} ORDER BY distinctCount DESC LIMIT {p7:Int32}",
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(statement.query_params["p0"]).toBe("principalUser");
|
|
244
|
+
expect(statement.query_params["p1"]).toBe("targetUser");
|
|
245
|
+
expect(statement.query_params["p6"]).toBe(3);
|
|
246
|
+
expect(statement.query_params["p7"]).toBe(MAX_GROUPS);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("threshold clamping", () => {
|
|
251
|
+
test("minMatchCount 0 clamps to 1 — no HAVING on a raw-count rule", async () => {
|
|
252
|
+
const spy: Spy = stubExecuteQuery([]);
|
|
253
|
+
|
|
254
|
+
await callFindDetectionMatches({ minMatchCount: 0 });
|
|
255
|
+
|
|
256
|
+
const statement: Statement = capturedStatement(spy);
|
|
257
|
+
|
|
258
|
+
/*
|
|
259
|
+
* Clamped-to-1 raw-count rules keep the HAVING-free shape: emitting
|
|
260
|
+
* `HAVING matchCount >= 1` would be harmless but is pinned away so
|
|
261
|
+
* the pre-#3398 query stays byte-identical for existing rules.
|
|
262
|
+
*/
|
|
263
|
+
expect(statement.query).not.toContain("HAVING");
|
|
264
|
+
expect(statement.query).toContain("ORDER BY matchCount DESC");
|
|
265
|
+
expect(statement.query_params["p4"]).toBe(MAX_GROUPS);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("a negative minMatchCount binds 1, not the negative value, on a distinct-count rule", async () => {
|
|
269
|
+
const spy: Spy = stubExecuteQuery([]);
|
|
270
|
+
|
|
271
|
+
await callFindDetectionMatches({
|
|
272
|
+
distinctCountExpression: buildFieldExpression("principalIp"),
|
|
273
|
+
minMatchCount: -5,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const statement: Statement = capturedStatement(spy);
|
|
277
|
+
|
|
278
|
+
expect(statement.query).toContain("HAVING distinctCount >= {p5:Int32}");
|
|
279
|
+
expect(statement.query_params["p5"]).toBe(1);
|
|
280
|
+
// The clamp must never leak a value <= 0 into any bound param.
|
|
281
|
+
expect(Object.values(statement.query_params)).not.toContain(-5);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("row mapping", () => {
|
|
286
|
+
test("maps every column, parsing ClickHouse's stringly counts", async () => {
|
|
287
|
+
/*
|
|
288
|
+
* ClickHouse's JSON format renders UInt64 aggregates as strings —
|
|
289
|
+
* matchCount and distinctCount must come back as numbers anyway.
|
|
290
|
+
*/
|
|
291
|
+
stubExecuteQuery([
|
|
292
|
+
{
|
|
293
|
+
groupValue: "alice",
|
|
294
|
+
matchCount: "7",
|
|
295
|
+
distinctCount: "3",
|
|
296
|
+
sampleMessage: "failed login for alice",
|
|
297
|
+
sampleObservables: ["alice", "10.0.0.9"],
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const groups: Array<DetectionMatchGroup> = await callFindDetectionMatches(
|
|
302
|
+
{
|
|
303
|
+
distinctCountExpression: buildFieldExpression("targetUser"),
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
expect(groups).toStrictEqual([
|
|
308
|
+
{
|
|
309
|
+
groupValue: "alice",
|
|
310
|
+
matchCount: 7,
|
|
311
|
+
distinctCount: 3,
|
|
312
|
+
sampleMessage: "failed login for alice",
|
|
313
|
+
sampleObservables: ["alice", "10.0.0.9"],
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("distinctCount defaults to 0 when the column is missing or null", async () => {
|
|
319
|
+
stubExecuteQuery([
|
|
320
|
+
{
|
|
321
|
+
// A row from before the column existed — no distinctCount key.
|
|
322
|
+
groupValue: "bob",
|
|
323
|
+
matchCount: 2,
|
|
324
|
+
sampleMessage: "m",
|
|
325
|
+
sampleObservables: [],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
groupValue: "eve",
|
|
329
|
+
matchCount: 4,
|
|
330
|
+
distinctCount: null,
|
|
331
|
+
sampleMessage: "n",
|
|
332
|
+
sampleObservables: [],
|
|
333
|
+
},
|
|
334
|
+
]);
|
|
335
|
+
|
|
336
|
+
const groups: Array<DetectionMatchGroup> =
|
|
337
|
+
await callFindDetectionMatches();
|
|
338
|
+
|
|
339
|
+
expect(groups[0]!.distinctCount).toBe(0);
|
|
340
|
+
expect(groups[1]!.distinctCount).toBe(0);
|
|
341
|
+
// ...without disturbing the fields that were there.
|
|
342
|
+
expect(groups[0]!.matchCount).toBe(2);
|
|
343
|
+
expect(groups[1]!.matchCount).toBe(4);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("degenerate rows fall back field-by-field instead of throwing", async () => {
|
|
347
|
+
stubExecuteQuery([
|
|
348
|
+
{
|
|
349
|
+
sampleObservables: "not-an-array",
|
|
350
|
+
},
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
const groups: Array<DetectionMatchGroup> =
|
|
354
|
+
await callFindDetectionMatches();
|
|
355
|
+
|
|
356
|
+
expect(groups).toStrictEqual([
|
|
357
|
+
{
|
|
358
|
+
groupValue: "",
|
|
359
|
+
matchCount: 0,
|
|
360
|
+
distinctCount: 0,
|
|
361
|
+
sampleMessage: "",
|
|
362
|
+
sampleObservables: [],
|
|
363
|
+
},
|
|
364
|
+
]);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("a response with no data key maps to an empty group list", async () => {
|
|
368
|
+
stubExecuteQuery(undefined);
|
|
369
|
+
|
|
370
|
+
const groups: Array<DetectionMatchGroup> =
|
|
371
|
+
await callFindDetectionMatches();
|
|
372
|
+
|
|
373
|
+
expect(groups).toStrictEqual([]);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|