@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,298 @@
|
|
|
1
|
+
import { describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
import timers from "timers";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* multer calls `setImmediate` internally (make-middleware.js and
|
|
6
|
+
* remove-uploaded-files.js), and Common's jest environment is jsdom, which
|
|
7
|
+
* does not expose it. A `@jest-environment node` docblock is not an option
|
|
8
|
+
* here - Common's shared jest.setup.ts touches `window`, which is why
|
|
9
|
+
* EsbuildConfig.test.ts spawns a subprocess instead - so lend jsdom the
|
|
10
|
+
* real one from Node.
|
|
11
|
+
*/
|
|
12
|
+
if (
|
|
13
|
+
typeof (globalThis as unknown as { setImmediate?: unknown }).setImmediate !==
|
|
14
|
+
"function"
|
|
15
|
+
) {
|
|
16
|
+
(globalThis as unknown as { setImmediate: unknown }).setImmediate =
|
|
17
|
+
timers.setImmediate;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* The limits under test are 25 and 50 MiB, so the fixtures that probe them
|
|
22
|
+
* are 25 and 50 MiB of real multipart body parsed by the real busboy.
|
|
23
|
+
* That does not fit in jest's 5s default.
|
|
24
|
+
*/
|
|
25
|
+
jest.setTimeout(180_000);
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* The multipart reader used by Pyroscope ingest and the SendGrid inbound
|
|
29
|
+
* email webhook.
|
|
30
|
+
*
|
|
31
|
+
* Both routes mount it BEFORE their auth check - Pyroscope before
|
|
32
|
+
* isAuthorizedServiceMiddleware, SendGrid before the webhook secret is
|
|
33
|
+
* verified - and it uses multer's memoryStorage, so every part it accepts
|
|
34
|
+
* is buffered in the process on behalf of a caller who has not proved
|
|
35
|
+
* anything yet.
|
|
36
|
+
*
|
|
37
|
+
* multer's defaults leave almost all of that unbounded: fileSize, files,
|
|
38
|
+
* fields and parts are all Infinity out of the box, and only fieldSize
|
|
39
|
+
* has a default at all. These tests pin the limits and the status code a
|
|
40
|
+
* breach produces, because the failure mode of getting this wrong is
|
|
41
|
+
* silent - an unbounded upload looks exactly like a working one until the
|
|
42
|
+
* pod dies.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import MultipartFormDataMiddleware, {
|
|
46
|
+
MAX_MULTIPART_FIELDS,
|
|
47
|
+
MAX_MULTIPART_FIELD_BYTES,
|
|
48
|
+
MAX_MULTIPART_FILES,
|
|
49
|
+
MAX_MULTIPART_FILE_BYTES,
|
|
50
|
+
} from "../../../Server/Middleware/MultipartFormData";
|
|
51
|
+
import {
|
|
52
|
+
ExpressRequest,
|
|
53
|
+
ExpressResponse,
|
|
54
|
+
NextFunction,
|
|
55
|
+
} from "../../../Server/Utils/Express";
|
|
56
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
57
|
+
import ExceptionCode from "../../../Types/Exception/ExceptionCode";
|
|
58
|
+
import { Readable } from "stream";
|
|
59
|
+
|
|
60
|
+
const BOUNDARY: string = "----oneuptimetestboundary";
|
|
61
|
+
|
|
62
|
+
interface MultipartPart {
|
|
63
|
+
name: string;
|
|
64
|
+
value: Buffer;
|
|
65
|
+
filename?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildMultipartBody(parts: Array<MultipartPart>): Buffer {
|
|
69
|
+
const chunks: Array<Buffer> = [];
|
|
70
|
+
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
const disposition: string = part.filename
|
|
73
|
+
? `form-data; name="${part.name}"; filename="${part.filename}"`
|
|
74
|
+
: `form-data; name="${part.name}"`;
|
|
75
|
+
|
|
76
|
+
const header: string =
|
|
77
|
+
`--${BOUNDARY}\r\n` +
|
|
78
|
+
`Content-Disposition: ${disposition}\r\n` +
|
|
79
|
+
(part.filename ? "Content-Type: application/octet-stream\r\n" : "") +
|
|
80
|
+
"\r\n";
|
|
81
|
+
|
|
82
|
+
chunks.push(Buffer.from(header));
|
|
83
|
+
chunks.push(part.value);
|
|
84
|
+
chunks.push(Buffer.from("\r\n"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
chunks.push(Buffer.from(`--${BOUNDARY}--\r\n`));
|
|
88
|
+
|
|
89
|
+
return Buffer.concat(chunks as unknown as Array<Uint8Array>);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface Outcome {
|
|
93
|
+
req: ExpressRequest;
|
|
94
|
+
error: unknown;
|
|
95
|
+
nextCalls: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* Drive the middleware with a real multipart body over a real Readable, so
|
|
100
|
+
* busboy does the parsing it does in production rather than being faked.
|
|
101
|
+
*/
|
|
102
|
+
function run(parts: Array<MultipartPart>): Promise<Outcome> {
|
|
103
|
+
const body: Buffer = buildMultipartBody(parts);
|
|
104
|
+
|
|
105
|
+
const req: Readable & {
|
|
106
|
+
headers: Record<string, string>;
|
|
107
|
+
body?: unknown;
|
|
108
|
+
files?: unknown;
|
|
109
|
+
} = Readable.from([body]) as never;
|
|
110
|
+
|
|
111
|
+
req.headers = {
|
|
112
|
+
"content-type": `multipart/form-data; boundary=${BOUNDARY}`,
|
|
113
|
+
"content-length": String(body.length),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const res: ExpressResponse = {
|
|
117
|
+
status: (): unknown => {
|
|
118
|
+
return { send: (): void => {}, json: (): void => {} };
|
|
119
|
+
},
|
|
120
|
+
headersSent: false,
|
|
121
|
+
} as unknown as ExpressResponse;
|
|
122
|
+
|
|
123
|
+
return new Promise<Outcome>((resolve: (value: Outcome) => void): void => {
|
|
124
|
+
let nextCalls: number = 0;
|
|
125
|
+
|
|
126
|
+
MultipartFormDataMiddleware(req as unknown as ExpressRequest, res, ((
|
|
127
|
+
err?: unknown,
|
|
128
|
+
): void => {
|
|
129
|
+
nextCalls++;
|
|
130
|
+
resolve({
|
|
131
|
+
req: req as unknown as ExpressRequest,
|
|
132
|
+
error: err,
|
|
133
|
+
nextCalls: nextCalls,
|
|
134
|
+
});
|
|
135
|
+
}) as NextFunction);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function files(outcome: Outcome): Array<{ fieldname: string; buffer: Buffer }> {
|
|
140
|
+
return (outcome.req.files || []) as Array<{
|
|
141
|
+
fieldname: string;
|
|
142
|
+
buffer: Buffer;
|
|
143
|
+
}>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("MultipartFormData - the limits are set where they were reasoned about", () => {
|
|
147
|
+
test("sizes match the app-wide 50 MiB body-parser number", () => {
|
|
148
|
+
expect(MAX_MULTIPART_FILE_BYTES).toBe(50 * 1024 * 1024);
|
|
149
|
+
expect(MAX_MULTIPART_FIELD_BYTES).toBe(25 * 1024 * 1024);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("counts are bounded at all, which is the part multer leaves at Infinity", () => {
|
|
153
|
+
expect(MAX_MULTIPART_FILES).toBe(50);
|
|
154
|
+
expect(MAX_MULTIPART_FIELDS).toBe(200);
|
|
155
|
+
expect(Number.isFinite(MAX_MULTIPART_FILES)).toBe(true);
|
|
156
|
+
expect(Number.isFinite(MAX_MULTIPART_FIELDS)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("MultipartFormData - normal bodies still parse", () => {
|
|
161
|
+
test("a single file arrives as a Buffer on req.files", async () => {
|
|
162
|
+
const outcome: Outcome = await run([
|
|
163
|
+
{ name: "profile", value: Buffer.from("pprof-bytes"), filename: "p.pb" },
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
expect(outcome.error).toBeUndefined();
|
|
167
|
+
expect(files(outcome)).toHaveLength(1);
|
|
168
|
+
expect(files(outcome)[0]!.fieldname).toBe("profile");
|
|
169
|
+
expect(files(outcome)[0]!.buffer.toString()).toBe("pprof-bytes");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("fields and files together, the SendGrid shape", async () => {
|
|
173
|
+
const outcome: Outcome = await run([
|
|
174
|
+
{ name: "from", value: Buffer.from("someone@example.com") },
|
|
175
|
+
{ name: "subject", value: Buffer.from("hello") },
|
|
176
|
+
{ name: "attachment1", value: Buffer.from("PDF"), filename: "a.pdf" },
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
expect(outcome.error).toBeUndefined();
|
|
180
|
+
expect((outcome.req.body as Record<string, string>)["from"]).toBe(
|
|
181
|
+
"someone@example.com",
|
|
182
|
+
);
|
|
183
|
+
expect(files(outcome)).toHaveLength(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("a body with no parts at all is fine", async () => {
|
|
187
|
+
const outcome: Outcome = await run([]);
|
|
188
|
+
|
|
189
|
+
expect(outcome.error).toBeUndefined();
|
|
190
|
+
expect(files(outcome)).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a file right at the count limit is accepted", async () => {
|
|
194
|
+
const outcome: Outcome = await run(
|
|
195
|
+
Array.from({ length: MAX_MULTIPART_FILES }, (_v: unknown, i: number) => {
|
|
196
|
+
return {
|
|
197
|
+
name: `f${i}`,
|
|
198
|
+
value: Buffer.from("x"),
|
|
199
|
+
filename: `f${i}.bin`,
|
|
200
|
+
};
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(outcome.error).toBeUndefined();
|
|
205
|
+
expect(files(outcome)).toHaveLength(MAX_MULTIPART_FILES);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("fields right at the count limit are accepted", async () => {
|
|
209
|
+
const outcome: Outcome = await run(
|
|
210
|
+
Array.from({ length: MAX_MULTIPART_FIELDS }, (_v: unknown, i: number) => {
|
|
211
|
+
return { name: `k${i}`, value: Buffer.from("v") };
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(outcome.error).toBeUndefined();
|
|
216
|
+
expect(
|
|
217
|
+
Object.keys(outcome.req.body as Record<string, unknown>),
|
|
218
|
+
).toHaveLength(MAX_MULTIPART_FIELDS);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("MultipartFormData - breaches answer 413, not 500", () => {
|
|
223
|
+
test("one file over the size limit", async () => {
|
|
224
|
+
const outcome: Outcome = await run([
|
|
225
|
+
{
|
|
226
|
+
name: "big",
|
|
227
|
+
value: Buffer.alloc(MAX_MULTIPART_FILE_BYTES + 1),
|
|
228
|
+
filename: "big.bin",
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
expect(outcome.error).toBeInstanceOf(Exception);
|
|
233
|
+
expect((outcome.error as Exception).code).toBe(
|
|
234
|
+
ExceptionCode.PayloadTooLargeException,
|
|
235
|
+
);
|
|
236
|
+
expect((outcome.error as Exception).code).toBe(413);
|
|
237
|
+
expect((outcome.error as Exception).message).toContain("LIMIT_FILE_SIZE");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("one file too many", async () => {
|
|
241
|
+
const outcome: Outcome = await run(
|
|
242
|
+
Array.from(
|
|
243
|
+
{ length: MAX_MULTIPART_FILES + 1 },
|
|
244
|
+
(_v: unknown, i: number) => {
|
|
245
|
+
return {
|
|
246
|
+
name: `f${i}`,
|
|
247
|
+
value: Buffer.from("x"),
|
|
248
|
+
filename: `f${i}.bin`,
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect((outcome.error as Exception).code).toBe(
|
|
255
|
+
ExceptionCode.PayloadTooLargeException,
|
|
256
|
+
);
|
|
257
|
+
expect((outcome.error as Exception).message).toContain("LIMIT_FILE_COUNT");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("one field too many - thousands of tiny parts is the cheap version of this attack", async () => {
|
|
261
|
+
const outcome: Outcome = await run(
|
|
262
|
+
Array.from(
|
|
263
|
+
{ length: MAX_MULTIPART_FIELDS + 1 },
|
|
264
|
+
(_v: unknown, i: number) => {
|
|
265
|
+
return { name: `k${i}`, value: Buffer.from("v") };
|
|
266
|
+
},
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect((outcome.error as Exception).code).toBe(
|
|
271
|
+
ExceptionCode.PayloadTooLargeException,
|
|
272
|
+
);
|
|
273
|
+
expect((outcome.error as Exception).message).toContain("LIMIT_FIELD_COUNT");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("one field value over the size limit", async () => {
|
|
277
|
+
const outcome: Outcome = await run([
|
|
278
|
+
{ name: "html", value: Buffer.alloc(MAX_MULTIPART_FIELD_BYTES + 1) },
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
expect((outcome.error as Exception).code).toBe(
|
|
282
|
+
ExceptionCode.PayloadTooLargeException,
|
|
283
|
+
);
|
|
284
|
+
expect((outcome.error as Exception).message).toContain("LIMIT_FIELD_VALUE");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("next() is called exactly once on a breach", async () => {
|
|
288
|
+
const outcome: Outcome = await run([
|
|
289
|
+
{
|
|
290
|
+
name: "big",
|
|
291
|
+
value: Buffer.alloc(MAX_MULTIPART_FILE_BYTES + 1),
|
|
292
|
+
filename: "big.bin",
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
expect(outcome.nextCalls).toBe(1);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -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
|
+
});
|