@oneuptime/common 12.0.23 → 12.0.25
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/AnalyticsModels/Span.ts +101 -0
- 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/BaseAPI.ts +0 -24
- package/Server/API/SlackAPI.ts +0 -2
- 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/Middleware/SlackAuthorization.ts +96 -18
- 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 +181 -0
- package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
- package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
- 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/LlmCallsTableIdentity.test.tsx +322 -0
- package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
- package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
- package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -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/BaseAPI.test.ts +41 -0
- package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
- 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/Middleware/SlackAuthorization.test.ts +262 -5
- 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 +387 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +386 -0
- package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -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 +584 -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 +770 -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/LlmConventions.ts +255 -0
- package/Types/Telemetry/LlmMetricConventions.ts +329 -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 +287 -0
- package/build/dist/Models/AnalyticsModels/Span.js +89 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- 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/BaseAPI.js +3 -19
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +0 -2
- package/build/dist/Server/API/SlackAPI.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/Middleware/SlackAuthorization.js +58 -7
- package/build/dist/Server/Middleware/SlackAuthorization.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 +151 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
- 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/LlmConventions.js +236 -0
- package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +290 -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 +175 -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,311 @@
|
|
|
1
|
+
import { AddDetectionRuleDistinctCountColumns1789512000000 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns";
|
|
2
|
+
import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
|
|
3
|
+
import DetectionRule from "../../../Models/DatabaseModels/DetectionRule";
|
|
4
|
+
import ColumnLength from "../../../Types/Database/ColumnLength";
|
|
5
|
+
import ColumnType from "../../../Types/Database/ColumnType";
|
|
6
|
+
import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
|
|
7
|
+
import TableColumnType from "../../../Types/Database/TableColumnType";
|
|
8
|
+
import {
|
|
9
|
+
MigrationInterface,
|
|
10
|
+
QueryRunner,
|
|
11
|
+
getMetadataArgsStorage,
|
|
12
|
+
} from "typeorm";
|
|
13
|
+
import type { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import { describe, expect, test } from "@jest/globals";
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* The migration behind detection-rule correlation semantics (issue #3398).
|
|
20
|
+
*
|
|
21
|
+
* DetectionRule gains distinctCountField (what a rule counts: unique
|
|
22
|
+
* values of a field instead of raw matching events) and
|
|
23
|
+
* matchCountThreshold (how many of that count a group needs in one
|
|
24
|
+
* evaluation window before the rule fires). Three things have to hold or
|
|
25
|
+
* a deployed instance breaks on upgrade:
|
|
26
|
+
*
|
|
27
|
+
* 1. up() adds exactly those two columns — distinctCountField nullable
|
|
28
|
+
* with NO default (NULL means "count raw events", the behavior every
|
|
29
|
+
* existing rule was saved with), matchCountThreshold NOT NULL
|
|
30
|
+
* DEFAULT '1' so every existing row keeps fire-on-any-match without
|
|
31
|
+
* a backfill;
|
|
32
|
+
* 2. down() removes exactly what up() added;
|
|
33
|
+
* 3. the migration is registered in SchemaMigrations/Index.ts — an
|
|
34
|
+
* unregistered migration never runs on boot, so the evaluator's
|
|
35
|
+
* SELECT of the two columns would 500 on every evaluation cycle.
|
|
36
|
+
*
|
|
37
|
+
* Pure SQL-contract assertions against a fake QueryRunner. No Postgres.
|
|
38
|
+
* The registry-wide ordering guard lives in
|
|
39
|
+
* SchemaMigrationsOrdering.test.ts and is not repeated here; what is
|
|
40
|
+
* asserted here is about THIS migration and stays true forever.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const MIGRATION_DIRECTORY: string = path.join(
|
|
44
|
+
__dirname,
|
|
45
|
+
"../../../Server/Infrastructure/Postgres/SchemaMigrations",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const MIGRATION_TIMESTAMP: string = "1789512000000";
|
|
49
|
+
|
|
50
|
+
const MIGRATION_NAME: string = `AddDetectionRuleDistinctCountColumns${MIGRATION_TIMESTAMP}`;
|
|
51
|
+
|
|
52
|
+
type MakeQueryRunnerResult = {
|
|
53
|
+
runner: QueryRunner;
|
|
54
|
+
statements: Array<string>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type MakeQueryRunnerFunction = () => MakeQueryRunnerResult;
|
|
58
|
+
|
|
59
|
+
const makeQueryRunner: MakeQueryRunnerFunction = (): MakeQueryRunnerResult => {
|
|
60
|
+
const statements: Array<string> = [];
|
|
61
|
+
|
|
62
|
+
const query: (...args: Array<unknown>) => Promise<undefined> = (
|
|
63
|
+
...args: Array<unknown>
|
|
64
|
+
): Promise<undefined> => {
|
|
65
|
+
statements.push(String(args[0]));
|
|
66
|
+
return Promise.resolve(undefined);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
runner: { query } as unknown as QueryRunner,
|
|
71
|
+
statements,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
describe("AddDetectionRuleDistinctCountColumns1789512000000 — SQL contract", () => {
|
|
76
|
+
test("up() adds exactly the two columns with their exact types and defaults", async () => {
|
|
77
|
+
const { runner, statements } = makeQueryRunner();
|
|
78
|
+
|
|
79
|
+
await new AddDetectionRuleDistinctCountColumns1789512000000().up(runner);
|
|
80
|
+
|
|
81
|
+
expect(statements).toEqual([
|
|
82
|
+
`ALTER TABLE "DetectionRule" ADD "distinctCountField" character varying(100)`,
|
|
83
|
+
`ALTER TABLE "DetectionRule" ADD "matchCountThreshold" integer NOT NULL DEFAULT '1'`,
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* NULL is a meaning on distinctCountField: it is what the evaluator
|
|
89
|
+
* reads as "threshold the raw match count", i.e. the semantics every
|
|
90
|
+
* rule saved before this migration was created with. A default would
|
|
91
|
+
* silently switch existing rules to distinct counting.
|
|
92
|
+
*/
|
|
93
|
+
test("distinctCountField is nullable with no default", async () => {
|
|
94
|
+
const { runner, statements } = makeQueryRunner();
|
|
95
|
+
|
|
96
|
+
await new AddDetectionRuleDistinctCountColumns1789512000000().up(runner);
|
|
97
|
+
|
|
98
|
+
const addDistinctCountField: string = statements.find((sql: string) => {
|
|
99
|
+
return sql.includes(`ADD "distinctCountField"`);
|
|
100
|
+
})!;
|
|
101
|
+
|
|
102
|
+
expect(addDistinctCountField).not.toContain("NOT NULL");
|
|
103
|
+
expect(addDistinctCountField).not.toContain("DEFAULT");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/*
|
|
107
|
+
* The opposite for matchCountThreshold: the DB default IS the
|
|
108
|
+
* compatibility story. Every pre-existing row gets threshold 1 —
|
|
109
|
+
* fire-on-any-match — the instant the column appears, with no backfill
|
|
110
|
+
* statement to forget.
|
|
111
|
+
*/
|
|
112
|
+
test("matchCountThreshold is NOT NULL with DEFAULT '1'", async () => {
|
|
113
|
+
const { runner, statements } = makeQueryRunner();
|
|
114
|
+
|
|
115
|
+
await new AddDetectionRuleDistinctCountColumns1789512000000().up(runner);
|
|
116
|
+
|
|
117
|
+
const addMatchCountThreshold: string = statements.find((sql: string) => {
|
|
118
|
+
return sql.includes(`ADD "matchCountThreshold"`);
|
|
119
|
+
})!;
|
|
120
|
+
|
|
121
|
+
expect(addMatchCountThreshold).toContain("integer NOT NULL DEFAULT '1'");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("down() drops exactly what up() added, in reverse order", async () => {
|
|
125
|
+
const { runner, statements } = makeQueryRunner();
|
|
126
|
+
|
|
127
|
+
await new AddDetectionRuleDistinctCountColumns1789512000000().down(runner);
|
|
128
|
+
|
|
129
|
+
expect(statements).toEqual([
|
|
130
|
+
`ALTER TABLE "DetectionRule" DROP COLUMN "matchCountThreshold"`,
|
|
131
|
+
`ALTER TABLE "DetectionRule" DROP COLUMN "distinctCountField"`,
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/*
|
|
136
|
+
* Not just "same count": the exact column names, derived from the two
|
|
137
|
+
* statement sets rather than restated, so adding a column to up()
|
|
138
|
+
* without adding it to down() fails here.
|
|
139
|
+
*/
|
|
140
|
+
test("up() and down() name the same columns", async () => {
|
|
141
|
+
const { runner: upRunner, statements: upStatements } = makeQueryRunner();
|
|
142
|
+
const { runner: downRunner, statements: downStatements } =
|
|
143
|
+
makeQueryRunner();
|
|
144
|
+
|
|
145
|
+
await new AddDetectionRuleDistinctCountColumns1789512000000().up(upRunner);
|
|
146
|
+
await new AddDetectionRuleDistinctCountColumns1789512000000().down(
|
|
147
|
+
downRunner,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const columnsIn: (
|
|
151
|
+
statements: Array<string>,
|
|
152
|
+
keyword: string,
|
|
153
|
+
) => Array<string> = (
|
|
154
|
+
statements: Array<string>,
|
|
155
|
+
keyword: string,
|
|
156
|
+
): Array<string> => {
|
|
157
|
+
return statements
|
|
158
|
+
.filter((sql: string) => {
|
|
159
|
+
return sql.includes(keyword);
|
|
160
|
+
})
|
|
161
|
+
.map((sql: string) => {
|
|
162
|
+
return sql.split(`${keyword} "`)[1]!.split('"')[0]!;
|
|
163
|
+
})
|
|
164
|
+
.sort();
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
expect(columnsIn(downStatements, "DROP COLUMN")).toEqual(
|
|
168
|
+
columnsIn(upStatements, "ADD"),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("registration", () => {
|
|
174
|
+
/*
|
|
175
|
+
* The step AGENTS.md calls out and the one that fails silently: a
|
|
176
|
+
* migration that is not in this array never runs, so the columns are
|
|
177
|
+
* missing in a real deployment while the evaluator's rule SELECT and
|
|
178
|
+
* the dashboard form both reference them.
|
|
179
|
+
*/
|
|
180
|
+
test("the migration is registered so it runs on boot", () => {
|
|
181
|
+
const names: Array<string> = SchemaMigrations.map(
|
|
182
|
+
(migration: new () => MigrationInterface): string => {
|
|
183
|
+
return migration.name;
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(names).toContain(MIGRATION_NAME);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("it is registered exactly once", () => {
|
|
191
|
+
const occurrences: number = SchemaMigrations.filter(
|
|
192
|
+
(migration: new () => MigrationInterface): boolean => {
|
|
193
|
+
return migration.name === MIGRATION_NAME;
|
|
194
|
+
},
|
|
195
|
+
).length;
|
|
196
|
+
|
|
197
|
+
expect(occurrences).toBe(1);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/*
|
|
201
|
+
* TypeORM records applied migrations by the `name` property, not the
|
|
202
|
+
* file name. A mismatch re-runs the migration on every boot.
|
|
203
|
+
*/
|
|
204
|
+
test("its declared name matches its class name", () => {
|
|
205
|
+
expect(new AddDetectionRuleDistinctCountColumns1789512000000().name).toBe(
|
|
206
|
+
MIGRATION_NAME,
|
|
207
|
+
);
|
|
208
|
+
expect(AddDetectionRuleDistinctCountColumns1789512000000.name).toBe(
|
|
209
|
+
MIGRATION_NAME,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("identity on disk", () => {
|
|
215
|
+
/*
|
|
216
|
+
* The timestamp in the class name is the one on disk. A class renamed
|
|
217
|
+
* without renaming its file — or two migrations landing on the same
|
|
218
|
+
* timestamp — makes the registered ordering describe something that is
|
|
219
|
+
* not what actually runs.
|
|
220
|
+
*/
|
|
221
|
+
test("exactly one file on disk carries its timestamp, and it is this one", () => {
|
|
222
|
+
const matching: Array<string> = fs
|
|
223
|
+
.readdirSync(MIGRATION_DIRECTORY)
|
|
224
|
+
.filter((file: string): boolean => {
|
|
225
|
+
return file.startsWith(`${MIGRATION_TIMESTAMP}-`);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(matching).toEqual([
|
|
229
|
+
`${MIGRATION_TIMESTAMP}-AddDetectionRuleDistinctCountColumns.ts`,
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
* The model side of the same contract. TypeORM generates the schema from
|
|
236
|
+
* these decorators, so a column that exists in the migration but not on
|
|
237
|
+
* the model (or vice versa, or with different nullability/defaults) is
|
|
238
|
+
* exactly the drift the Postgres Schema Drift workflow fails on.
|
|
239
|
+
*/
|
|
240
|
+
describe("the DetectionRule model carries the columns the migration adds", () => {
|
|
241
|
+
const columnArgs: (propertyName: string) => ColumnMetadataArgs = (
|
|
242
|
+
propertyName: string,
|
|
243
|
+
): ColumnMetadataArgs => {
|
|
244
|
+
const args: ColumnMetadataArgs | undefined = getMetadataArgsStorage()
|
|
245
|
+
.columns.filter((candidate: ColumnMetadataArgs) => {
|
|
246
|
+
return candidate.target === DetectionRule;
|
|
247
|
+
})
|
|
248
|
+
.find((candidate: ColumnMetadataArgs) => {
|
|
249
|
+
return candidate.propertyName === propertyName;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (!args) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`DetectionRule.${propertyName} has no TypeORM @Column metadata`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return args;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
test("distinctCountField is a nullable ShortText varchar(100), matching the migration", () => {
|
|
262
|
+
const args: ColumnMetadataArgs = columnArgs("distinctCountField");
|
|
263
|
+
|
|
264
|
+
expect(args.options.type).toBe(ColumnType.ShortText);
|
|
265
|
+
expect(args.options.length).toBe(ColumnLength.ShortText);
|
|
266
|
+
// character varying(100) in the migration.
|
|
267
|
+
expect(ColumnLength.ShortText).toBe(100);
|
|
268
|
+
expect(args.options.nullable).toBe(true);
|
|
269
|
+
expect(args.options.default).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("matchCountThreshold is a NOT NULL number defaulting to 1, matching the migration", () => {
|
|
273
|
+
const args: ColumnMetadataArgs = columnArgs("matchCountThreshold");
|
|
274
|
+
|
|
275
|
+
expect(args.options.type).toBe(ColumnType.Number);
|
|
276
|
+
expect(args.options.nullable).toBe(false);
|
|
277
|
+
expect(args.options.default).toBe(1);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("distinctCountField's TableColumn is an optional ShortText", () => {
|
|
281
|
+
const metadata: TableColumnMetadata =
|
|
282
|
+
new DetectionRule().getTableColumnMetadata("distinctCountField");
|
|
283
|
+
|
|
284
|
+
expect(metadata.type).toBe(TableColumnType.ShortText);
|
|
285
|
+
expect(metadata.required).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
/*
|
|
289
|
+
* isDefaultValueColumn is the API-side half of the DB default: without
|
|
290
|
+
* it, checkRequiredFields 400s every create payload written before the
|
|
291
|
+
* column existed and the DEFAULT '1' can never apply.
|
|
292
|
+
*/
|
|
293
|
+
test("matchCountThreshold's TableColumn is required with default 1 as a default-value column", () => {
|
|
294
|
+
const metadata: TableColumnMetadata =
|
|
295
|
+
new DetectionRule().getTableColumnMetadata("matchCountThreshold");
|
|
296
|
+
|
|
297
|
+
expect(metadata.type).toBe(TableColumnType.Number);
|
|
298
|
+
expect(metadata.required).toBe(true);
|
|
299
|
+
expect(metadata.defaultValue).toBe(1);
|
|
300
|
+
expect(metadata.isDefaultValueColumn).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("both columns default to undefined on a fresh model instance", () => {
|
|
304
|
+
const rule: DetectionRule = new DetectionRule();
|
|
305
|
+
|
|
306
|
+
expect("distinctCountField" in rule).toBe(true);
|
|
307
|
+
expect("matchCountThreshold" in rule).toBe(true);
|
|
308
|
+
expect(rule.distinctCountField).toBeUndefined();
|
|
309
|
+
expect(rule.matchCountThreshold).toBeUndefined();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -13,8 +13,10 @@ import { describe, expect, test } from "@jest/globals";
|
|
|
13
13
|
* typo must surface to the person editing the rule, not as a cron-side
|
|
14
14
|
* lastError hours later. These tests drive the create/update hooks
|
|
15
15
|
* directly: invalid or aggregation-based Sigma is rejected, the rule name
|
|
16
|
-
* defaults from the Sigma title,
|
|
17
|
-
* whole minutes between 1 and 1440
|
|
16
|
+
* defaults from the Sigma title, the evaluation interval is clamped to
|
|
17
|
+
* whole minutes between 1 and 1440, and the match count threshold must be
|
|
18
|
+
* a whole number between 1 and 1000000 (or omitted — the column defaults
|
|
19
|
+
* to 1, so old API clients that never send it keep working).
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
22
|
const PROJECT_ID: ObjectID = new ObjectID(
|
|
@@ -52,6 +54,7 @@ function buildRule(
|
|
|
52
54
|
name?: string;
|
|
53
55
|
sigmaRuleYaml?: string;
|
|
54
56
|
evaluationIntervalInMinutes?: number;
|
|
57
|
+
matchCountThreshold?: number;
|
|
55
58
|
} = {},
|
|
56
59
|
): DetectionRule {
|
|
57
60
|
const rule: DetectionRule = new DetectionRule();
|
|
@@ -66,6 +69,10 @@ function buildRule(
|
|
|
66
69
|
rule.evaluationIntervalInMinutes = options.evaluationIntervalInMinutes;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
if (options.matchCountThreshold !== undefined) {
|
|
73
|
+
rule.matchCountThreshold = options.matchCountThreshold;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
return rule;
|
|
70
77
|
}
|
|
71
78
|
|
|
@@ -219,6 +226,58 @@ detection:
|
|
|
219
226
|
).resolves.toBeDefined();
|
|
220
227
|
});
|
|
221
228
|
});
|
|
229
|
+
|
|
230
|
+
describe("match count threshold validation", () => {
|
|
231
|
+
const THRESHOLD_ERROR: string =
|
|
232
|
+
"Match count threshold must be a whole number between 1 and 1000000.";
|
|
233
|
+
|
|
234
|
+
test.each([1, 5, 1000000])("%d is accepted", async (threshold: number) => {
|
|
235
|
+
const rule: DetectionRule = buildRule({
|
|
236
|
+
name: "Thresholded",
|
|
237
|
+
matchCountThreshold: threshold,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await expect(
|
|
241
|
+
service.onBeforeCreate(buildCreateBy(rule)),
|
|
242
|
+
).resolves.toBeDefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test.each([0, -1, 1.5, 1000001])(
|
|
246
|
+
"%d is rejected with the exact message",
|
|
247
|
+
async (threshold: number) => {
|
|
248
|
+
const rule: DetectionRule = buildRule({
|
|
249
|
+
name: "Bad Threshold",
|
|
250
|
+
matchCountThreshold: threshold,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await expect(
|
|
254
|
+
service.onBeforeCreate(buildCreateBy(rule)),
|
|
255
|
+
).rejects.toThrow(THRESHOLD_ERROR);
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
test("an omitted threshold passes — the column defaults to 1, so old API clients that never send it still create rules", async () => {
|
|
260
|
+
const rule: DetectionRule = buildRule({ name: "No Threshold" });
|
|
261
|
+
|
|
262
|
+
expect(rule.matchCountThreshold).toBeUndefined();
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
service.onBeforeCreate(buildCreateBy(rule)),
|
|
266
|
+
).resolves.toBeDefined();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("a valid threshold does not bypass the Sigma validation that runs before it", async () => {
|
|
270
|
+
const rule: DetectionRule = buildRule({
|
|
271
|
+
name: "Still Broken YAML",
|
|
272
|
+
sigmaRuleYaml: "title: [unclosed",
|
|
273
|
+
matchCountThreshold: 5,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await expect(service.onBeforeCreate(buildCreateBy(rule))).rejects.toThrow(
|
|
277
|
+
BadDataException,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
222
281
|
});
|
|
223
282
|
|
|
224
283
|
describe("DetectionRuleService.onBeforeUpdate", () => {
|
|
@@ -251,4 +310,77 @@ describe("DetectionRuleService.onBeforeUpdate", () => {
|
|
|
251
310
|
),
|
|
252
311
|
).rejects.toThrow(BadDataException);
|
|
253
312
|
});
|
|
313
|
+
|
|
314
|
+
test("an update with an invalid matchCountThreshold is rejected with the exact message", async () => {
|
|
315
|
+
await expect(
|
|
316
|
+
service.onBeforeUpdate(buildUpdateBy({ matchCountThreshold: 0 })),
|
|
317
|
+
).rejects.toThrow(
|
|
318
|
+
"Match count threshold must be a whole number between 1 and 1000000.",
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("an update with a valid matchCountThreshold passes", async () => {
|
|
323
|
+
await expect(
|
|
324
|
+
service.onBeforeUpdate(buildUpdateBy({ matchCountThreshold: 42 })),
|
|
325
|
+
).resolves.toBeDefined();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("an update that omits matchCountThreshold skips the threshold validation", async () => {
|
|
329
|
+
await expect(
|
|
330
|
+
service.onBeforeUpdate(buildUpdateBy({ description: "No threshold" })),
|
|
331
|
+
).resolves.toBeDefined();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("DetectionRuleService field-name trimming", () => {
|
|
336
|
+
/*
|
|
337
|
+
* groupByField / distinctCountField are looked up verbatim by the
|
|
338
|
+
* evaluator — an unknown name silently becomes an attributes[] lookup
|
|
339
|
+
* yielding '' on every row, and for a distinct-count rule that is a
|
|
340
|
+
* permanent zero-fire outage with no error anywhere. Trimming at save
|
|
341
|
+
* time kills the whitespace-from-a-paste class of that failure.
|
|
342
|
+
*/
|
|
343
|
+
test("create trims whitespace from groupByField and distinctCountField", async () => {
|
|
344
|
+
const rule: DetectionRule = buildRule();
|
|
345
|
+
rule.groupByField = " principalIp ";
|
|
346
|
+
rule.distinctCountField = "principalUser\t";
|
|
347
|
+
|
|
348
|
+
const result: OnCreate<DetectionRule> = await service.onBeforeCreate(
|
|
349
|
+
buildCreateBy(rule),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
expect(result.createBy.data.groupByField).toBe("principalIp");
|
|
353
|
+
expect(result.createBy.data.distinctCountField).toBe("principalUser");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("create leaves omitted field names undefined", async () => {
|
|
357
|
+
const result: OnCreate<DetectionRule> = await service.onBeforeCreate(
|
|
358
|
+
buildCreateBy(buildRule()),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
expect(result.createBy.data.groupByField).toBeUndefined();
|
|
362
|
+
expect(result.createBy.data.distinctCountField).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("update trims field names and collapses whitespace-only to the cleared value", async () => {
|
|
366
|
+
const result: OnUpdate<DetectionRule> = await service.onBeforeUpdate(
|
|
367
|
+
buildUpdateBy({
|
|
368
|
+
groupByField: " ",
|
|
369
|
+
distinctCountField: " targetUser ",
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// '' is the documented "feature off" value, so clearing still works.
|
|
374
|
+
expect(result.updateBy.data.groupByField).toBe("");
|
|
375
|
+
expect(result.updateBy.data.distinctCountField).toBe("targetUser");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("an update that omits the field names does not add them", async () => {
|
|
379
|
+
const result: OnUpdate<DetectionRule> = await service.onBeforeUpdate(
|
|
380
|
+
buildUpdateBy({ description: "No field names" }),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
expect("groupByField" in result.updateBy.data).toBe(false);
|
|
384
|
+
expect("distinctCountField" in result.updateBy.data).toBe(false);
|
|
385
|
+
});
|
|
254
386
|
});
|
|
@@ -140,7 +140,13 @@ describe("discovery-scan claim hookless write safety preconditions", () => {
|
|
|
140
140
|
"startedAt",
|
|
141
141
|
"statusMessage",
|
|
142
142
|
];
|
|
143
|
-
|
|
143
|
+
/*
|
|
144
|
+
* `name` joined `cidr` here when discovery scans became nameable
|
|
145
|
+
* (issue #3391): the hook validates and normalizes it on any update that
|
|
146
|
+
* carries it. The claim payload does not, which is what keeps the
|
|
147
|
+
* hookless write honest.
|
|
148
|
+
*/
|
|
149
|
+
const columnsValidatedByHook: Array<string> = ["cidr", "name"];
|
|
144
150
|
|
|
145
151
|
for (const column of claimWriteColumns) {
|
|
146
152
|
expect(columnsValidatedByHook).not.toContain(column);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Retention windows are declared per service via hardDeleteItemsOlderThanInDays,
|
|
6
|
+
* but they only take effect once the service is listed in
|
|
7
|
+
* Server/Services/Index.ts - that array is what
|
|
8
|
+
* App/FeatureSet/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts iterates.
|
|
9
|
+
* A window on an unregistered service is dead configuration, and registering
|
|
10
|
+
* the service later silently arms it.
|
|
11
|
+
*
|
|
12
|
+
* That is not hypothetical: OnCallDutyPolicyFeedService sat unregistered
|
|
13
|
+
* carrying a 90 day window while every other feed kept 3 years. Commit
|
|
14
|
+
* 6bbf2f866c ("retain items for 3 years instead of 120 days") swept the feeds
|
|
15
|
+
* from 120 to 3 * 365 and missed this one precisely because it held 90 rather
|
|
16
|
+
* than 120, and nothing noticed because the service was not registered.
|
|
17
|
+
* Registering it - which the workflow component registry fix had to do, since
|
|
18
|
+
* OnCallDutyPolicyFeed is workflow-enabled - would have started hard deleting
|
|
19
|
+
* on-call policy history at 90 days on billing-enabled deployments.
|
|
20
|
+
*
|
|
21
|
+
* Feed tables are the same kind of data as each other: a user-visible activity
|
|
22
|
+
* timeline hung off a parent entity. They should expire together, whether or
|
|
23
|
+
* not they happen to be registered yet.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const SERVICES_DIRECTORY: string = path.join(
|
|
27
|
+
__dirname,
|
|
28
|
+
"..",
|
|
29
|
+
"..",
|
|
30
|
+
"..",
|
|
31
|
+
"Server",
|
|
32
|
+
"Services",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const RETENTION_PATTERN: RegExp =
|
|
36
|
+
/hardDeleteItemsOlderThanInDays\(\s*"(\w+)"\s*,\s*([^)]+?)\s*\)/;
|
|
37
|
+
|
|
38
|
+
interface FeedRetention {
|
|
39
|
+
serviceName: string;
|
|
40
|
+
columnName: string;
|
|
41
|
+
window: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const feedServiceNames: Array<string> = fs
|
|
45
|
+
.readdirSync(SERVICES_DIRECTORY)
|
|
46
|
+
.sort()
|
|
47
|
+
.filter((fileName: string) => {
|
|
48
|
+
return fileName.endsWith("FeedService.ts");
|
|
49
|
+
})
|
|
50
|
+
.map((fileName: string) => {
|
|
51
|
+
return fileName.replace(/\.ts$/, "");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const feedRetentions: Array<FeedRetention> = [];
|
|
55
|
+
|
|
56
|
+
for (const serviceName of feedServiceNames) {
|
|
57
|
+
const source: string = fs.readFileSync(
|
|
58
|
+
path.join(SERVICES_DIRECTORY, `${serviceName}.ts`),
|
|
59
|
+
"utf8",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const match: RegExpMatchArray | null = source.match(RETENTION_PATTERN);
|
|
63
|
+
|
|
64
|
+
if (!match) {
|
|
65
|
+
// Deliberately not skipped - see "declares a retention window at all" below.
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
feedRetentions.push({
|
|
70
|
+
serviceName: serviceName,
|
|
71
|
+
columnName: match[1]!,
|
|
72
|
+
window: match[2]!.trim(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("Feed service retention windows", () => {
|
|
77
|
+
test("the feed services were actually found", () => {
|
|
78
|
+
// Guards the assertions below against a silent pass on an empty list.
|
|
79
|
+
expect(feedServiceNames.length).toBeGreaterThan(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("every feed service declares a retention window at all", () => {
|
|
83
|
+
/*
|
|
84
|
+
* The window/column assertions below can only speak for services this file
|
|
85
|
+
* managed to parse. Comparing against the filenames on disk - rather than a
|
|
86
|
+
* hardcoded count - is what stops a feed service that declares no retention,
|
|
87
|
+
* or declares one in a shape the pattern misses, from being dropped from
|
|
88
|
+
* those assertions and passing in silence.
|
|
89
|
+
*/
|
|
90
|
+
const parsed: Array<string> = feedRetentions.map((entry: FeedRetention) => {
|
|
91
|
+
return entry.serviceName;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(parsed).toEqual(feedServiceNames);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("every feed service expires on the same window", () => {
|
|
98
|
+
const windows: Array<string> = [
|
|
99
|
+
...new Set(
|
|
100
|
+
feedRetentions.map((entry: FeedRetention) => {
|
|
101
|
+
return entry.window;
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Reported with the owning service names so a divergent one is obvious.
|
|
107
|
+
expect({
|
|
108
|
+
windows,
|
|
109
|
+
services: feedRetentions.map((entry: FeedRetention) => {
|
|
110
|
+
return `${entry.serviceName}=${entry.window}`;
|
|
111
|
+
}),
|
|
112
|
+
}).toEqual({
|
|
113
|
+
windows: ["3 * 365"],
|
|
114
|
+
services: feedRetentions.map((entry: FeedRetention) => {
|
|
115
|
+
return `${entry.serviceName}=3 * 365`;
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("every feed service expires on the same column", () => {
|
|
121
|
+
const columns: Array<string> = [
|
|
122
|
+
...new Set(
|
|
123
|
+
feedRetentions.map((entry: FeedRetention) => {
|
|
124
|
+
return entry.columnName;
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
expect(columns).toEqual(["createdAt"]);
|
|
130
|
+
});
|
|
131
|
+
});
|