@oneuptime/common 13.0.7 → 13.0.8
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/OnCallDutyPolicyScheduleCalendarFeed.ts +1 -1
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +1 -1
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +1 -1
- package/Server/API/AIBillingAPI.ts +1 -0
- package/Server/API/AIInsightAPI.ts +1 -4
- package/Server/API/AIInvestigationAPI.ts +1 -4
- package/Server/API/AIReadinessAPI.ts +4 -5
- package/Server/API/AlertAPI.ts +1 -0
- package/Server/API/AutoRemediationAPI.ts +1 -4
- package/Server/API/BaseAPI.ts +13 -0
- package/Server/API/BaseAnalyticsAPI.ts +80 -9
- package/Server/API/BillingAPI.ts +2 -0
- package/Server/API/BillingInvoiceAPI.ts +1 -0
- package/Server/API/BillingPaymentMethodAPI.ts +2 -0
- package/Server/API/CodeFixRunAPI.ts +1 -3
- package/Server/API/CommonAPI.ts +68 -4
- package/Server/API/DashboardAPI.ts +12 -12
- package/Server/API/GlobalConfigAPI.ts +17 -0
- package/Server/API/IncidentAPI.ts +2 -0
- package/Server/API/IncidentEpisodeAPI.ts +1 -0
- package/Server/API/NotificationAPI.ts +1 -0
- package/Server/API/OnCallCalendarAPI.ts +4 -5
- package/Server/API/OnCallDutyPolicyAPI.ts +7 -0
- package/Server/API/OnCallReadinessAPI.ts +4 -1
- package/Server/API/OnCallScheduleTimelineAPI.ts +731 -0
- package/Server/API/ProjectAPI.ts +1 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +1 -0
- package/Server/API/StatusPageAPI.ts +161 -32
- package/Server/API/TeamMemberAPI.ts +3 -0
- package/Server/API/UserCallAPI.ts +2 -0
- package/Server/API/UserEmailAPI.ts +2 -0
- package/Server/API/UserIncomingCallNumberAPI.ts +2 -0
- package/Server/API/UserMicrosoftTeamsAPI.ts +1 -0
- package/Server/API/UserSlackAPI.ts +1 -0
- package/Server/API/UserSmsAPI.ts +2 -0
- package/Server/API/UserTotpAuthAPI.ts +1 -0
- package/Server/API/UserWebAuthnAPI.ts +2 -0
- package/Server/API/UserWebhookAPI.ts +1 -0
- package/Server/API/UserWhatsAppAPI.ts +2 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +13 -2
- package/Server/Middleware/UserAuthorization.ts +79 -5
- package/Server/Services/ApiKeyPermissionService.ts +101 -4
- package/Server/Services/DatabaseService.ts +245 -0
- package/Server/Services/DetectionRuleService.ts +5 -4
- package/Server/Services/OnCallDutyPolicyChildService.ts +261 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -2
- package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +2 -2
- package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -2
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -2
- package/Server/Services/PayAsYouGoBillingService.ts +23 -28
- package/Server/Services/ProjectService.ts +18 -0
- package/Server/Services/RumSessionPinService.ts +274 -22
- package/Server/Services/TeamPermissionService.ts +120 -26
- package/Server/Services/ThreatIntelFeedService.ts +6 -3
- package/Server/Types/Database/Permissions/BasePermission.ts +9 -7
- package/Server/Types/Database/Permissions/Index.ts +75 -11
- package/Server/Types/Database/Permissions/TablePermission.ts +24 -28
- package/Server/Utils/AI/AIChatPrivacyFilter.ts +8 -0
- package/Server/Utils/AI/AIRunPrivacyFilter.ts +8 -0
- package/Server/Utils/MasterAdminPageAccess.ts +61 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +3 -1
- package/Server/Utils/Monitor/MonitorIncident.ts +3 -1
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1 -1
- package/Server/Utils/Realtime.ts +163 -45
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +11 -28
- package/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.ts +170 -0
- package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +1 -1
- package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.ts +6 -2
- package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.ts +10 -9
- package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +1059 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerEditForm.test.tsx +376 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +757 -0
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +307 -0
- package/Tests/App/Dashboard/ArgumentsFormGroupByUnlistedAttributes.test.tsx +131 -0
- package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +1 -1
- package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +9 -1
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +24 -0
- package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +162 -8
- package/Tests/App/Dashboard/EntityFilterDropdownArchivedSlos.test.tsx +105 -6
- package/Tests/App/Dashboard/EntityFilterDropdownUnlistedSelection.test.tsx +434 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +276 -1
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +9 -1
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +58 -42
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +42 -5
- package/Tests/App/Dashboard/ResourceAuditLogsSideMenus.test.tsx +34 -23
- package/Tests/App/Dashboard/ScheduleTimeline.test.tsx +1580 -0
- package/Tests/App/Dashboard/ScheduleTimelineAPI.test.ts +280 -0
- package/Tests/App/Dashboard/ScheduleTimelineModel.test.ts +673 -0
- package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +95 -68
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +86 -3
- package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +28 -3
- package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +761 -0
- package/Tests/App/Dashboard/SecurityEventsGuideContent.test.ts +1313 -0
- package/Tests/App/Dashboard/SecurityEventsHowItWorksCard.test.tsx +536 -0
- package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +222 -0
- package/Tests/App/Dashboard/SecurityEventsThreatIntelPage.test.tsx +199 -0
- package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +321 -0
- package/Tests/App/Dashboard/SloOverviewHero.test.tsx +171 -36
- package/Tests/App/Dashboard/SloViewSideMenu.test.tsx +215 -0
- package/Tests/App/Dashboard/SloViewSideMenuCounts.test.tsx +40 -3
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +12 -4
- package/Tests/App/PublicDashboard/PublicDashboardWidgetListAuth.test.tsx +623 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +23 -1
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +138 -5
- package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +187 -0
- package/Tests/Server/API/AIInvestigationAvailabilityAuthorization.test.ts +51 -3
- package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +55 -2
- package/Tests/Server/API/BaseAnalyticsAPIAggregateCache.test.ts +729 -0
- package/Tests/Server/API/BillingPayAsYouGoStatus.test.ts +93 -7
- package/Tests/Server/API/BillingPaymentMethodAPI.test.ts +36 -3
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +1003 -24
- package/Tests/Server/API/DashboardPublicRateLimit.test.ts +6 -3
- package/Tests/Server/API/ExpiredSessionAIRoutes.test.ts +708 -0
- package/Tests/Server/API/ExpiredSessionCustomRoutes.test.ts +928 -0
- package/Tests/Server/API/ExpiredSessionMonitorTemplate.test.ts +319 -0
- package/Tests/Server/API/ExpiredSessionOnCallDutyPolicy.test.ts +336 -0
- package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +820 -0
- package/Tests/Server/API/GlobalConfigLicense.test.ts +140 -0
- package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +115 -12
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +375 -8
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +175 -17
- package/Tests/Server/API/OnCallScheduleTimelineAPI.test.ts +1513 -0
- package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +119 -7
- package/Tests/Server/API/ProjectScopedCustomRouteAuth.test.ts +111 -4
- package/Tests/Server/API/PublicRouteInvalidAccessToken.test.ts +615 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +89 -2
- package/Tests/Server/API/SessionReplayAPI.test.ts +121 -0
- package/Tests/Server/API/StatusPageMasterPasswordRateLimit.test.ts +4 -2
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +441 -0
- package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +664 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +7 -0
- package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +6 -0
- package/Tests/Server/API/UserNotificationSettingAPI.test.ts +65 -4
- package/Tests/Server/API/UserProjectsAPI.test.ts +63 -3
- package/Tests/Server/API/UserTotpAuthAPI.test.ts +48 -2
- package/Tests/Server/API/UserWebAuthnAuthenticationOptionsRoute.test.ts +11 -2
- package/Tests/Server/API/UserWebAuthnBackupCodeMinting.test.ts +46 -3
- package/Tests/Server/API/UserWebAuthnPasskeyAPI.test.ts +18 -2
- package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +24 -7
- package/Tests/Server/Middleware/ExpiredSessionUserMiddleware.test.ts +563 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +299 -19
- package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +453 -0
- package/Tests/Server/Services/AIChatPrivacyPin.test.ts +24 -0
- package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +2 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +3 -3
- package/Tests/Server/Services/DatabaseServiceCreateSuppliedId.test.ts +147 -0
- package/Tests/Server/Services/DatabaseServiceLoginBeforeHooks.test.ts +1078 -0
- package/Tests/Server/Services/DatabaseServiceTenantRelationGuard.test.ts +259 -0
- package/Tests/Server/Services/DatabaseServiceWriteOperatorValues.test.ts +147 -0
- package/Tests/Server/Services/MonitorPayAsYouGoBilling.test.ts +115 -1
- package/Tests/Server/Services/OnCallChildCreateAuthorizationPostgres.test.ts +711 -0
- package/Tests/Server/Services/OnCallDutyPolicyChildService.test.ts +733 -0
- package/Tests/Server/Services/PayAsYouGoBillingService.test.ts +190 -140
- package/Tests/Server/Services/PayAsYouGoStripeRateLimitBudget.test.ts +4 -9
- package/Tests/Server/Services/PermissionBlockDeletion.test.ts +780 -0
- package/Tests/Server/Services/PermissionBlockDeletionPostgres.test.ts +628 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +77 -1
- package/Tests/Server/Services/RumSessionPinCreatePermission.test.ts +715 -0
- package/Tests/Server/Services/TeamMemberMultiTenantReadPostgres.test.ts +394 -0
- package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +6 -0
- package/Tests/Server/Services/TelemetryPayAsYouGoBilling.test.ts +131 -0
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +13 -2
- package/Tests/Server/Types/Database/Permissions/ModelPermissionAnonymousRefusal.test.ts +518 -0
- package/Tests/Server/Types/Database/Permissions/MultiTenantPermissions.test.ts +350 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +282 -25
- package/Tests/Server/Types/Database/Permissions/TableLevelBlockPermission.test.ts +459 -0
- package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +101 -9
- package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +88 -4
- package/Tests/Server/Utils/AI/Toolbox/ExceptionTools.test.ts +459 -0
- package/Tests/Server/Utils/AI/Toolbox/LogTools.test.ts +794 -0
- package/Tests/Server/Utils/AI/Toolbox/RecentChangesTools.test.ts +574 -0
- package/Tests/Server/Utils/AI/Toolbox/SecurityEventTools.test.ts +596 -0
- package/Tests/Server/Utils/CaptchaRequestAndLogging.test.ts +395 -0
- package/Tests/Server/Utils/MasterAdminPageAccess.test.ts +251 -0
- package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +616 -0
- package/Tests/Server/Utils/Realtime.test.ts +502 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorSeverity.test.ts +216 -0
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +142 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.test.ts +140 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepairClickhouseIntegration.test.ts +542 -0
- package/Tests/Server/Utils/Telemetry/ChangeEventRow.test.ts +743 -0
- package/Tests/Server/Utils/Telemetry/RuntimeMetrics.test.ts +465 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Alert.test.ts +202 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Incident.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/ScheduledMaintenance.test.ts +231 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/Alert.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/AlertEpisode.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/Incident.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/ScheduledMaintenance.test.ts +247 -0
- package/Tests/Types/AI/AIInsightEvidence.test.ts +206 -0
- package/Tests/Types/BaseDatabase/DatabaseCommonInteractionPropsUtilAnonymous.test.ts +144 -0
- package/Tests/Types/Call/CallProvider.test.ts +528 -0
- package/Tests/Types/Kubernetes/KubernetesCostIngest.test.ts +236 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleTimeline.test.ts +736 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleTimelineLayout.test.ts +996 -0
- package/Tests/Types/SecurityEvent/SigmaRule.test.ts +142 -0
- package/Tests/Types/Workflow/Components/JSON.test.ts +182 -0
- package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +116 -1
- package/Tests/UI/Components/Dropdown/DropdownAsyncOptions.test.tsx +81 -0
- package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +89 -1
- package/Tests/UI/Components/EventAttachmentList.test.tsx +174 -0
- package/Tests/UI/Components/Forms/BasicFormBurnRateTemplateDefaults.test.tsx +395 -0
- package/Tests/UI/Components/Forms/BasicFormCollapsibleSections.test.tsx +286 -0
- package/Tests/UI/Components/Forms/FormSummary.test.tsx +128 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableQueryChange.test.tsx +365 -0
- package/Tests/UI/Components/OpenAuthenticatedUrl.test.ts +358 -0
- package/Tests/UI/Components/SessionAwareImage.test.tsx +496 -0
- package/Tests/UI/Components/TelemetryHistogramZeroBuckets.test.tsx +190 -0
- package/Tests/UI/Components/TooltipInteractive.test.tsx +97 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardInstallGuidance.test.tsx +1 -1
- package/Tests/UI/Monitor/EvaluationLogListTimestamps.test.tsx +367 -0
- package/Tests/UI/Utils/API/BaseAPIRefreshSession.test.ts +1087 -0
- package/Tests/UI/Utils/API/DashboardHost.ts +12 -0
- package/Tests/UI/Utils/API/FakeAxiosServer.ts +183 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/Cookie.test.ts +440 -0
- package/Tests/UI/Utils/ModelAPIRequestClient.test.ts +278 -0
- package/Tests/UI/Utils/Navigation.test.ts +33 -0
- package/Tests/UI/Utils/PricingPlan.test.ts +295 -0
- package/Tests/UI/Utils/Realtime.test.ts +759 -0
- package/Tests/UI/Utils/Telemetry/Telemetry.test.ts +500 -0
- package/Tests/Utils/APIInstanceDispatch.test.ts +311 -0
- package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizerSeverity.test.ts +565 -0
- package/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.ts +40 -0
- package/Types/Monitor/MonitorEvaluationSummary.ts +3 -0
- package/Types/OnCallDutyPolicy/ScheduleTimeline.ts +507 -0
- package/Types/OnCallDutyPolicy/ScheduleTimelineLayout.ts +589 -0
- package/Types/Realtime/EventName.ts +11 -0
- package/Types/SecurityEvent/DetectionFindingConstants.ts +14 -0
- package/Types/SecurityEvent/OcsfSeverity.ts +31 -26
- package/Types/SecurityEvent/SigmaRule.ts +51 -0
- package/Types/SecurityEvent/ThreatIntelConstants.ts +18 -0
- package/UI/Components/AttachmentList/EventAttachmentList.tsx +30 -2
- package/UI/Components/Dashboard/DashboardVariableControl.tsx +12 -2
- package/UI/Components/Dropdown/Dropdown.tsx +6 -1
- package/UI/Components/EditionLabel/EditionLabel.tsx +12 -0
- package/UI/Components/EventItem/EventItem.tsx +12 -1
- package/UI/Components/Forms/BasicForm.tsx +151 -126
- package/UI/Components/Forms/CollapsibleFormSection.tsx +63 -0
- package/UI/Components/Forms/Fields/FormField.tsx +10 -4
- package/UI/Components/Forms/FormSummary.tsx +7 -0
- package/UI/Components/Forms/Types/Field.ts +10 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +2 -1
- package/UI/Components/Markdown.tsx/SessionAwareImage.tsx +122 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +34 -5
- package/UI/Components/ModelTable/TableView.tsx +1 -1
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +8 -1
- package/UI/Components/Tooltip/Tooltip.tsx +8 -1
- package/UI/Utils/API/API.ts +173 -12
- package/UI/Utils/API/RequestOptions.ts +11 -0
- package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +31 -7
- package/UI/Utils/ModelAPI/ModelAPI.ts +30 -9
- package/UI/Utils/Navigation.ts +9 -0
- package/UI/Utils/OpenAuthenticatedUrl.ts +154 -0
- package/UI/Utils/Realtime.ts +176 -30
- package/Utils/API.ts +14 -5
- package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +157 -7
- package/Utils/SecurityEvent/Sigma/SigmaRuleParser.ts +9 -17
- package/Utils/Slo/SloBurnRateTemplate.ts +2 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +1 -1
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -1
- package/build/dist/Server/API/AIBillingAPI.js +1 -1
- package/build/dist/Server/API/AIBillingAPI.js.map +1 -1
- package/build/dist/Server/API/AIInsightAPI.js +1 -4
- package/build/dist/Server/API/AIInsightAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +1 -4
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AIReadinessAPI.js +1 -3
- package/build/dist/Server/API/AIReadinessAPI.js.map +1 -1
- package/build/dist/Server/API/AlertAPI.js +1 -1
- package/build/dist/Server/API/AlertAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +1 -4
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +12 -0
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/BaseAnalyticsAPI.js +55 -10
- package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
- package/build/dist/Server/API/BillingAPI.js +2 -2
- package/build/dist/Server/API/BillingAPI.js.map +1 -1
- package/build/dist/Server/API/BillingInvoiceAPI.js +1 -1
- package/build/dist/Server/API/BillingInvoiceAPI.js.map +1 -1
- package/build/dist/Server/API/BillingPaymentMethodAPI.js +2 -2
- package/build/dist/Server/API/BillingPaymentMethodAPI.js.map +1 -1
- package/build/dist/Server/API/CodeFixRunAPI.js +1 -3
- package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +50 -5
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +12 -12
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +12 -0
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +2 -2
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
- package/build/dist/Server/API/IncidentEpisodeAPI.js.map +1 -1
- package/build/dist/Server/API/NotificationAPI.js +1 -1
- package/build/dist/Server/API/NotificationAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallCalendarAPI.js +1 -4
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallDutyPolicyAPI.js +6 -0
- package/build/dist/Server/API/OnCallDutyPolicyAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallReadinessAPI.js +4 -1
- package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +494 -0
- package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -0
- package/build/dist/Server/API/ProjectAPI.js +1 -1
- package/build/dist/Server/API/ProjectAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +125 -33
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +3 -3
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +2 -2
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +2 -2
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js +2 -2
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/UserSlackAPI.js +1 -1
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +2 -2
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserTotpAuthAPI.js +1 -1
- package/build/dist/Server/API/UserTotpAuthAPI.js.map +1 -1
- package/build/dist/Server/API/UserWebAuthnAPI.js +2 -2
- package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
- package/build/dist/Server/API/UserWebhookAPI.js +1 -1
- package/build/dist/Server/API/UserWebhookAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +2 -2
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -2
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +64 -4
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +80 -4
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +171 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DetectionRuleService.js +4 -4
- package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyChildService.js +170 -0
- package/build/dist/Server/Services/OnCallDutyPolicyChildService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -2
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +2 -2
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -2
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -2
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/PayAsYouGoBillingService.js +20 -27
- package/build/dist/Server/Services/PayAsYouGoBillingService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +17 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RumSessionPinService.js +180 -16
- package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
- package/build/dist/Server/Services/TeamPermissionService.js +89 -27
- package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
- package/build/dist/Server/Services/ThreatIntelFeedService.js +5 -4
- package/build/dist/Server/Services/ThreatIntelFeedService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js +9 -6
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/Index.js +60 -6
- package/build/dist/Server/Types/Database/Permissions/Index.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js +14 -13
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +7 -0
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +7 -0
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -1
- package/build/dist/Server/Utils/MasterAdminPageAccess.js +39 -0
- package/build/dist/Server/Utils/MasterAdminPageAccess.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +3 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +3 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Realtime.js +126 -37
- package/build/dist/Server/Utils/Realtime.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +7 -25
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js +127 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js +3 -2
- package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js +4 -8
- package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js.map +1 -1
- package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js +31 -1
- package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js +321 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js +372 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js.map +1 -0
- package/build/dist/Types/Realtime/EventName.js +10 -0
- package/build/dist/Types/Realtime/EventName.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +11 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/SecurityEvent/OcsfSeverity.js +30 -25
- package/build/dist/Types/SecurityEvent/OcsfSeverity.js.map +1 -1
- package/build/dist/Types/SecurityEvent/SigmaRule.js +47 -0
- package/build/dist/Types/SecurityEvent/SigmaRule.js.map +1 -1
- package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js +14 -0
- package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js.map +1 -1
- package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js +9 -4
- package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js.map +1 -1
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +10 -2
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +5 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +11 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/EventItem/EventItem.js +2 -2
- package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +70 -64
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/CollapsibleFormSection.js +27 -0
- package/build/dist/UI/Components/Forms/CollapsibleFormSection.js.map +1 -0
- package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -7
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/FormSummary.js +7 -0
- package/build/dist/UI/Components/Forms/FormSummary.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +2 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js +84 -0
- package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +14 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +8 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +2 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/UI/Utils/API/API.js +143 -19
- package/build/dist/UI/Utils/API/API.js.map +1 -1
- package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +21 -7
- package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +26 -9
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +8 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/OpenAuthenticatedUrl.js +92 -0
- package/build/dist/UI/Utils/OpenAuthenticatedUrl.js.map +1 -0
- package/build/dist/UI/Utils/Realtime.js +120 -15
- package/build/dist/UI/Utils/Realtime.js.map +1 -1
- package/build/dist/Utils/API.js +13 -5
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +123 -6
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
- package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js +4 -18
- package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js.map +1 -1
- package/build/dist/Utils/Slo/SloBurnRateTemplate.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The alerting module pulls the native isolated-vm addon through its
|
|
3
|
+
* template renderer; stub it out before anything imports it.
|
|
4
|
+
*/
|
|
5
|
+
jest.mock("isolated-vm", () => {
|
|
6
|
+
return {};
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "@jest/globals";
|
|
10
|
+
import DetectionRulesGuide, {
|
|
11
|
+
DETECTION_RULE_EXAMPLE_YAML,
|
|
12
|
+
DETECTION_THRESHOLD_EXAMPLES,
|
|
13
|
+
DetectionThresholdExample,
|
|
14
|
+
SIGMA_COLUMN_DOCS,
|
|
15
|
+
SIGMA_FIELD_ALIAS_DOCS,
|
|
16
|
+
SIGMA_MODIFIER_DOCS,
|
|
17
|
+
SIGMA_SELECTIONS_EXAMPLE_YAML,
|
|
18
|
+
SigmaFieldDoc,
|
|
19
|
+
SigmaModifierDoc,
|
|
20
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/HowItWorks/DetectionRulesGuide";
|
|
21
|
+
import ThreatIntelGuide, {
|
|
22
|
+
INDICATOR_TYPE_DOCS,
|
|
23
|
+
SUPPORTED_COMBINED_PATTERN_EXAMPLES,
|
|
24
|
+
THREAT_INTEL_SIGMA_EXAMPLE_YAML,
|
|
25
|
+
THREAT_LEVEL_BANDS,
|
|
26
|
+
ThreatLevelBand,
|
|
27
|
+
UNSCORED_CONFIDENCE,
|
|
28
|
+
UNSUPPORTED_PATTERN_EXAMPLES,
|
|
29
|
+
formatBand,
|
|
30
|
+
threatLevelForBand,
|
|
31
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/HowItWorks/ThreatIntelGuide";
|
|
32
|
+
import {
|
|
33
|
+
SecurityEventsGuide,
|
|
34
|
+
SecurityEventsGuideLevel,
|
|
35
|
+
SecurityEventsGuideSection,
|
|
36
|
+
getGuideSection,
|
|
37
|
+
guideToMarkdown,
|
|
38
|
+
markdownTable,
|
|
39
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/HowItWorks/SecurityEventsGuide";
|
|
40
|
+
import SigmaRuleParser from "../../../Utils/SecurityEvent/Sigma/SigmaRuleParser";
|
|
41
|
+
import SigmaClickhouseCompiler, {
|
|
42
|
+
FIELD_ALIASES,
|
|
43
|
+
buildSigmaDistinctCountExpression,
|
|
44
|
+
buildSigmaFieldExpression,
|
|
45
|
+
resolveSigmaField,
|
|
46
|
+
} from "../../../Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler";
|
|
47
|
+
import { pickSeverityByPrecedence } from "../../../Server/Utils/SecurityEvent/SecurityEventAlerting";
|
|
48
|
+
import { Statement } from "../../../Server/Utils/AnalyticsDatabase/Statement";
|
|
49
|
+
import StixPatternParser, {
|
|
50
|
+
ParsedIndicatorValue,
|
|
51
|
+
} from "../../../Utils/SecurityEvent/ThreatIntel/StixPatternParser";
|
|
52
|
+
import SigmaRule, {
|
|
53
|
+
SIGMA_DEFAULT_LEVEL,
|
|
54
|
+
SIGMA_LEVEL_TO_OCSF_SEVERITY,
|
|
55
|
+
SIGMA_SUPPORTED_MODIFIERS,
|
|
56
|
+
SigmaFieldRequirement,
|
|
57
|
+
SigmaLevel,
|
|
58
|
+
isSevereSigmaLevel,
|
|
59
|
+
} from "../../../Types/SecurityEvent/SigmaRule";
|
|
60
|
+
import OcsfSeverity from "../../../Types/SecurityEvent/OcsfSeverity";
|
|
61
|
+
import {
|
|
62
|
+
DETECTION_DISTINCT_COUNT_ATTRIBUTE,
|
|
63
|
+
DETECTION_EVALUATION_INTERVAL_MAX_IN_MINUTES,
|
|
64
|
+
DETECTION_EVALUATION_INTERVAL_MIN_IN_MINUTES,
|
|
65
|
+
DETECTION_FINDING_CLASS_NAME,
|
|
66
|
+
DETECTION_FINDING_CLASS_UID,
|
|
67
|
+
DETECTION_GROUP_VALUE_ATTRIBUTE,
|
|
68
|
+
DETECTION_MATCH_COUNT_ATTRIBUTE,
|
|
69
|
+
DETECTION_MATCH_COUNT_THRESHOLD_MAX,
|
|
70
|
+
DETECTION_MATCH_COUNT_THRESHOLD_MIN,
|
|
71
|
+
DETECTION_MAX_GROUPS_PER_EVALUATION,
|
|
72
|
+
DETECTION_MAX_LOOKBACK_IN_MINUTES,
|
|
73
|
+
DETECTION_RULE_ID_ATTRIBUTE,
|
|
74
|
+
DETECTION_RULE_NAME_ATTRIBUTE,
|
|
75
|
+
DETECTION_SIGMA_ID_ATTRIBUTE,
|
|
76
|
+
} from "../../../Types/SecurityEvent/DetectionFindingConstants";
|
|
77
|
+
import {
|
|
78
|
+
ENRICHMENT_CONFIDENCE_ATTRIBUTE,
|
|
79
|
+
ENRICHMENT_FEED_ATTRIBUTE,
|
|
80
|
+
ENRICHMENT_FEED_ID_ATTRIBUTE,
|
|
81
|
+
ENRICHMENT_INDICATOR_ID_ATTRIBUTE,
|
|
82
|
+
ENRICHMENT_INDICATOR_TYPE_ATTRIBUTE,
|
|
83
|
+
ENRICHMENT_INDICATOR_VALUE_ATTRIBUTE,
|
|
84
|
+
ENRICHMENT_MATCHED_ATTRIBUTE,
|
|
85
|
+
ENRICHMENT_MATCH_COUNT_ATTRIBUTE,
|
|
86
|
+
THREAT_CONFIDENCE_ATTRIBUTE,
|
|
87
|
+
THREAT_FEED_ID_ATTRIBUTE,
|
|
88
|
+
THREAT_FEED_NAME_ATTRIBUTE,
|
|
89
|
+
THREAT_INDICATOR_ID_ATTRIBUTE,
|
|
90
|
+
THREAT_INDICATOR_TYPE_ATTRIBUTE,
|
|
91
|
+
THREAT_INDICATOR_VALUE_ATTRIBUTE,
|
|
92
|
+
THREAT_INTEL_DEFAULT_POLL_INTERVAL_IN_MINUTES,
|
|
93
|
+
THREAT_INTEL_DEFAULT_VALID_DAYS,
|
|
94
|
+
THREAT_INTEL_FIRST_MATCH_WINDOW_IN_MINUTES,
|
|
95
|
+
THREAT_INTEL_MATCH_MAX_LOOKBACK_IN_MINUTES,
|
|
96
|
+
THREAT_INTEL_MAX_INDICATORS_PER_EVALUATION,
|
|
97
|
+
THREAT_INTEL_MINIMUM_CONFIDENCE_MAX,
|
|
98
|
+
THREAT_INTEL_MINIMUM_CONFIDENCE_MIN,
|
|
99
|
+
THREAT_INTEL_POLL_INTERVAL_MAX_IN_MINUTES,
|
|
100
|
+
THREAT_INTEL_POLL_INTERVAL_MIN_IN_MINUTES,
|
|
101
|
+
THREAT_INTEL_PRODUCT_NAME,
|
|
102
|
+
THREAT_MATCH_COUNT_ATTRIBUTE,
|
|
103
|
+
ThreatIntelIndicatorType,
|
|
104
|
+
ocsfSeverityForConfidence,
|
|
105
|
+
} from "../../../Types/SecurityEvent/ThreatIntelConstants";
|
|
106
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* The Detection Rules and Threat Intel guides are what a customer reads to
|
|
110
|
+
* decide whether to trust a detection — so a guide that is wrong is worse
|
|
111
|
+
* than no guide. Nothing about a wrong sentence fails to render, so every
|
|
112
|
+
* claim the guides make is pinned here against the code that actually
|
|
113
|
+
* behaves that way: examples go through the real Sigma parser, compiler
|
|
114
|
+
* and STIX pattern parser; tables are compared with the engine's own
|
|
115
|
+
* mappings, in both directions where a table claims to be complete; and
|
|
116
|
+
* every number is checked against the constant the engine runs on.
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
const GUIDES: Array<SecurityEventsGuide> = [
|
|
120
|
+
DetectionRulesGuide,
|
|
121
|
+
ThreatIntelGuide,
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const GUIDE_CASES: Array<[string, SecurityEventsGuide]> = GUIDES.map(
|
|
125
|
+
(guide: SecurityEventsGuide): [string, SecurityEventsGuide] => {
|
|
126
|
+
return [guide.id, guide];
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const MODIFIER_CASES: Array<[string, SigmaModifierDoc]> =
|
|
131
|
+
SIGMA_MODIFIER_DOCS.map(
|
|
132
|
+
(doc: SigmaModifierDoc): [string, SigmaModifierDoc] => {
|
|
133
|
+
return [doc.modifier, doc];
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const ALIAS_CASES: Array<[string, string]> = SIGMA_FIELD_ALIAS_DOCS.flatMap(
|
|
138
|
+
(doc: SigmaFieldDoc): Array<[string, string]> => {
|
|
139
|
+
return doc.aliases.map((alias: string): [string, string] => {
|
|
140
|
+
return [alias, doc.column];
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const THRESHOLD_CASES: Array<[string, DetectionThresholdExample]> =
|
|
146
|
+
DETECTION_THRESHOLD_EXAMPLES.map(
|
|
147
|
+
(
|
|
148
|
+
example: DetectionThresholdExample,
|
|
149
|
+
): [string, DetectionThresholdExample] => {
|
|
150
|
+
return [example.goal, example];
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const BAND_CASES: Array<[string, ThreatLevelBand]> = THREAT_LEVEL_BANDS.map(
|
|
155
|
+
(band: ThreatLevelBand): [string, ThreatLevelBand] => {
|
|
156
|
+
return [formatBand(band), band];
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const SIGMA_LEVELS_IN_ORDER: Array<SigmaLevel> = [
|
|
161
|
+
SigmaLevel.Informational,
|
|
162
|
+
SigmaLevel.Low,
|
|
163
|
+
SigmaLevel.Medium,
|
|
164
|
+
SigmaLevel.High,
|
|
165
|
+
SigmaLevel.Critical,
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const YAML_BLOCK_PATTERN: RegExp = /```yaml\n([\s\S]*?)```/g;
|
|
169
|
+
const FENCE_PATTERN: RegExp = /```/g;
|
|
170
|
+
|
|
171
|
+
function sectionMarkdown(
|
|
172
|
+
guide: SecurityEventsGuide,
|
|
173
|
+
sectionId: string,
|
|
174
|
+
): string {
|
|
175
|
+
const section: SecurityEventsGuideSection | undefined = getGuideSection(
|
|
176
|
+
guide,
|
|
177
|
+
sectionId,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(section).toBeDefined();
|
|
181
|
+
|
|
182
|
+
return section!.markdown;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function allMarkdown(guide: SecurityEventsGuide): string {
|
|
186
|
+
return guide.sections
|
|
187
|
+
.map((section: SecurityEventsGuideSection): string => {
|
|
188
|
+
return section.markdown;
|
|
189
|
+
})
|
|
190
|
+
.join("\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ruleWithSelectionLine(line: string): string {
|
|
194
|
+
return `title: Guide example
|
|
195
|
+
detection:
|
|
196
|
+
selection:
|
|
197
|
+
${line}
|
|
198
|
+
condition: selection
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function compileLine(line: string): Statement {
|
|
203
|
+
return SigmaClickhouseCompiler.compileYaml(ruleWithSelectionLine(line));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function paramValues(statement: Statement): Array<unknown> {
|
|
207
|
+
return Object.values(statement.query_params);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function yamlBlocks(markdown: string): Array<string> {
|
|
211
|
+
return Array.from(markdown.matchAll(YAML_BLOCK_PATTERN)).map(
|
|
212
|
+
(match: RegExpMatchArray): string => {
|
|
213
|
+
return match[1] || "";
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
describe("Security Events guides: structure", () => {
|
|
219
|
+
test.each(GUIDE_CASES)(
|
|
220
|
+
"%s has a title, summary, four steps and a levels strip",
|
|
221
|
+
(_id: string, guide: SecurityEventsGuide) => {
|
|
222
|
+
expect(guide.title.trim()).not.toBe("");
|
|
223
|
+
expect(guide.summary.trim()).not.toBe("");
|
|
224
|
+
expect(guide.steps).toHaveLength(4);
|
|
225
|
+
|
|
226
|
+
for (const step of guide.steps) {
|
|
227
|
+
expect(step.title.trim()).not.toBe("");
|
|
228
|
+
expect(step.description.trim()).not.toBe("");
|
|
229
|
+
expect(step.icon).toBeDefined();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expect(guide.levels.items.length).toBeGreaterThan(0);
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
test.each(GUIDE_CASES)(
|
|
237
|
+
"%s has unique section ids and titles (the tabs are keyed by title)",
|
|
238
|
+
(_id: string, guide: SecurityEventsGuide) => {
|
|
239
|
+
const ids: Array<string> = guide.sections.map(
|
|
240
|
+
(section: SecurityEventsGuideSection) => {
|
|
241
|
+
return section.id;
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
const titles: Array<string> = guide.sections.map(
|
|
245
|
+
(section: SecurityEventsGuideSection) => {
|
|
246
|
+
return section.title;
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
251
|
+
expect(new Set(titles).size).toBe(titles.length);
|
|
252
|
+
expect(guide.sections[0]?.id).toBe("overview");
|
|
253
|
+
expect(guide.sections[guide.sections.length - 1]?.id).toBe(
|
|
254
|
+
"troubleshooting",
|
|
255
|
+
);
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
test.each(GUIDE_CASES)(
|
|
260
|
+
"%s's levels strip links to a section that exists",
|
|
261
|
+
(_id: string, guide: SecurityEventsGuide) => {
|
|
262
|
+
expect(getGuideSection(guide, guide.levels.sectionId)).toBeDefined();
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
test.each(GUIDE_CASES)(
|
|
267
|
+
"%s's markdown carries no failed interpolation",
|
|
268
|
+
(_id: string, guide: SecurityEventsGuide) => {
|
|
269
|
+
const text: string = [
|
|
270
|
+
guide.title,
|
|
271
|
+
guide.summary,
|
|
272
|
+
guide.guideTitle,
|
|
273
|
+
guide.guideDescription,
|
|
274
|
+
guide.levels.title,
|
|
275
|
+
guide.levels.description,
|
|
276
|
+
...guide.steps.map((step: { description: string }) => {
|
|
277
|
+
return step.description;
|
|
278
|
+
}),
|
|
279
|
+
allMarkdown(guide),
|
|
280
|
+
].join("\n");
|
|
281
|
+
|
|
282
|
+
expect(text).not.toContain("undefined");
|
|
283
|
+
expect(text).not.toContain("NaN");
|
|
284
|
+
expect(text).not.toContain("[object Object]");
|
|
285
|
+
expect(text).not.toContain("${");
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
test.each(GUIDE_CASES)(
|
|
290
|
+
"%s never leaves a code fence open (an open fence swallows the rest of the tab)",
|
|
291
|
+
(_id: string, guide: SecurityEventsGuide) => {
|
|
292
|
+
for (const section of guide.sections) {
|
|
293
|
+
const fences: number = (section.markdown.match(FENCE_PATTERN) || [])
|
|
294
|
+
.length;
|
|
295
|
+
expect(fences % 2).toBe(0);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
test.each(GUIDE_CASES)(
|
|
301
|
+
"every YAML sample in %s is a Sigma rule the save-time validator accepts",
|
|
302
|
+
(_id: string, guide: SecurityEventsGuide) => {
|
|
303
|
+
const blocks: Array<string> = yamlBlocks(allMarkdown(guide));
|
|
304
|
+
|
|
305
|
+
expect(blocks.length).toBeGreaterThan(0);
|
|
306
|
+
|
|
307
|
+
for (const block of blocks) {
|
|
308
|
+
expect(() => {
|
|
309
|
+
SigmaClickhouseCompiler.compileYaml(block);
|
|
310
|
+
}).not.toThrow();
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
test("the two guides use different ids, so their collapsed state is stored apart", () => {
|
|
316
|
+
expect(DetectionRulesGuide.id).not.toBe(ThreatIntelGuide.id);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("each guide points at a public documentation path", () => {
|
|
320
|
+
expect(DetectionRulesGuide.documentationPath).toBe(
|
|
321
|
+
"/telemetry/security-events",
|
|
322
|
+
);
|
|
323
|
+
expect(ThreatIntelGuide.documentationPath).toBe(
|
|
324
|
+
"/telemetry/threat-intelligence",
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("guideToMarkdown", () => {
|
|
330
|
+
test("includes every section, headed by its title, in order", () => {
|
|
331
|
+
const markdown: string = guideToMarkdown(DetectionRulesGuide);
|
|
332
|
+
|
|
333
|
+
let lastIndex: number = -1;
|
|
334
|
+
|
|
335
|
+
for (const section of DetectionRulesGuide.sections) {
|
|
336
|
+
const headingIndex: number = markdown.indexOf(`### ${section.title}\n`);
|
|
337
|
+
expect(headingIndex).toBeGreaterThan(lastIndex);
|
|
338
|
+
expect(markdown).toContain(section.markdown.trim());
|
|
339
|
+
lastIndex = headingIndex;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("separates sections with a rule", () => {
|
|
344
|
+
const markdown: string = guideToMarkdown(ThreatIntelGuide);
|
|
345
|
+
|
|
346
|
+
expect(markdown.split("\n\n---\n\n")).toHaveLength(
|
|
347
|
+
ThreatIntelGuide.sections.length,
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("markdownTable", () => {
|
|
353
|
+
test("renders a header, a separator and one line per row", () => {
|
|
354
|
+
expect(
|
|
355
|
+
markdownTable(
|
|
356
|
+
["A", "B"],
|
|
357
|
+
[
|
|
358
|
+
["1", "2"],
|
|
359
|
+
["3", "4"],
|
|
360
|
+
],
|
|
361
|
+
),
|
|
362
|
+
).toBe("| A | B |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("escapes pipes so a Sigma field|modifier key cannot split its cell", () => {
|
|
366
|
+
expect(markdownTable(["Key"], [["`CommandLine|contains`"]])).toBe(
|
|
367
|
+
"| Key |\n| --- |\n| `CommandLine\\|contains` |",
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("Detection Rules guide: the walkthrough rule", () => {
|
|
373
|
+
test("parses and compiles, so the example would save", () => {
|
|
374
|
+
expect(() => {
|
|
375
|
+
SigmaClickhouseCompiler.compileYaml(DETECTION_RULE_EXAMPLE_YAML);
|
|
376
|
+
}).not.toThrow();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("is high level, as the overview says", () => {
|
|
380
|
+
expect(SigmaRuleParser.parse(DETECTION_RULE_EXAMPLE_YAML).level).toBe(
|
|
381
|
+
SigmaLevel.High,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("its ATT&CK id tags become the finding's technique and tactic, as the tags row says", () => {
|
|
386
|
+
const rule: SigmaRule = SigmaRuleParser.parse(DETECTION_RULE_EXAMPLE_YAML);
|
|
387
|
+
|
|
388
|
+
expect(rule.mitreTechniques).toEqual(["T1110"]);
|
|
389
|
+
expect(rule.mitreTactics).toEqual(["TA0006"]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("named ATT&CK tags are not mapped, as the tags row warns", () => {
|
|
393
|
+
const rule: SigmaRule = SigmaRuleParser.parse(`title: Named tags
|
|
394
|
+
tags:
|
|
395
|
+
- attack.credential_access
|
|
396
|
+
detection:
|
|
397
|
+
selection:
|
|
398
|
+
className: Authentication
|
|
399
|
+
condition: selection
|
|
400
|
+
`);
|
|
401
|
+
|
|
402
|
+
expect(rule.mitreTactics).toEqual([]);
|
|
403
|
+
expect(rule.mitreTechniques).toEqual([]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("excludes internal IPs the way the overview describes", () => {
|
|
407
|
+
const statement: Statement = SigmaClickhouseCompiler.compileYaml(
|
|
408
|
+
DETECTION_RULE_EXAMPLE_YAML,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
expect(statement.query).toContain("NOT ");
|
|
412
|
+
expect(statement.query).toContain("principalIp ILIKE");
|
|
413
|
+
expect(paramValues(statement)).toContain("10.%");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("the overview walks through it with a Group By Field and threshold the engine accepts", () => {
|
|
417
|
+
const overview: string = sectionMarkdown(DetectionRulesGuide, "overview");
|
|
418
|
+
|
|
419
|
+
expect(overview).toContain(DETECTION_RULE_EXAMPLE_YAML);
|
|
420
|
+
expect(resolveSigmaField("principalIp").kind).toBe("textColumn");
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe("Detection Rules guide: selections and conditions", () => {
|
|
425
|
+
test("the selections sample has the three shapes it describes", () => {
|
|
426
|
+
const rule: SigmaRule = SigmaRuleParser.parse(
|
|
427
|
+
SIGMA_SELECTIONS_EXAMPLE_YAML,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const byName: Map<string, SigmaRule["selections"][number]> = new Map(
|
|
431
|
+
rule.selections.map((selection: SigmaRule["selections"][number]) => {
|
|
432
|
+
return [selection.name, selection];
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// Field map: one map whose statusName carries two OR-ed values.
|
|
437
|
+
const logon: SigmaRule["selections"][number] | undefined =
|
|
438
|
+
byName.get("selection_logon");
|
|
439
|
+
expect(logon?.fieldMaps).toHaveLength(1);
|
|
440
|
+
const statusName: SigmaFieldRequirement | undefined =
|
|
441
|
+
logon?.fieldMaps[0]?.find((requirement: SigmaFieldRequirement) => {
|
|
442
|
+
return requirement.field === "statusName";
|
|
443
|
+
});
|
|
444
|
+
expect(statusName?.values).toEqual(["Failure", "Blocked"]);
|
|
445
|
+
|
|
446
|
+
// List of maps: two alternatives.
|
|
447
|
+
expect(byName.get("selection_admin")?.fieldMaps).toHaveLength(2);
|
|
448
|
+
|
|
449
|
+
// Keywords.
|
|
450
|
+
expect(byName.get("keywords")?.keywords).toEqual([
|
|
451
|
+
"password spray",
|
|
452
|
+
"credential stuffing",
|
|
453
|
+
]);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("keywords match anywhere in the message, ignoring case", () => {
|
|
457
|
+
const statement: Statement = SigmaClickhouseCompiler.compileYaml(
|
|
458
|
+
SIGMA_SELECTIONS_EXAMPLE_YAML,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(statement.query).toContain("message ILIKE");
|
|
462
|
+
expect(paramValues(statement)).toContain("%password spray%");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("the documented quantifiers are all accepted", () => {
|
|
466
|
+
for (const condition of [
|
|
467
|
+
"1 of selection_*",
|
|
468
|
+
"all of selection_*",
|
|
469
|
+
"any of them",
|
|
470
|
+
"all of them",
|
|
471
|
+
"selection_a and not (selection_b or selection_c)",
|
|
472
|
+
]) {
|
|
473
|
+
expect(() => {
|
|
474
|
+
SigmaClickhouseCompiler.compileYaml(`title: Quantifiers
|
|
475
|
+
detection:
|
|
476
|
+
selection_a:
|
|
477
|
+
className: Authentication
|
|
478
|
+
selection_b:
|
|
479
|
+
statusName: Failure
|
|
480
|
+
selection_c:
|
|
481
|
+
principalUser: root
|
|
482
|
+
condition: ${condition}
|
|
483
|
+
`);
|
|
484
|
+
}).not.toThrow();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("aggregations are rejected at save, as the guide says", () => {
|
|
489
|
+
expect(() => {
|
|
490
|
+
SigmaRuleParser.parse(`title: Aggregation
|
|
491
|
+
detection:
|
|
492
|
+
selection:
|
|
493
|
+
className: Authentication
|
|
494
|
+
condition: selection | count() by principalIp > 5
|
|
495
|
+
`);
|
|
496
|
+
}).toThrow(/aggregation/i);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("a timeframe under detection is accepted and ignored", () => {
|
|
500
|
+
const withTimeframe: Statement =
|
|
501
|
+
SigmaClickhouseCompiler.compileYaml(`title: T
|
|
502
|
+
detection:
|
|
503
|
+
selection:
|
|
504
|
+
className: Authentication
|
|
505
|
+
timeframe: 5m
|
|
506
|
+
condition: selection
|
|
507
|
+
`);
|
|
508
|
+
const without: Statement = SigmaClickhouseCompiler.compileYaml(
|
|
509
|
+
ruleWithSelectionLine("className: Authentication"),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
expect(withTimeframe.query).toBe(without.query);
|
|
513
|
+
expect(withTimeframe.query_params).toEqual(without.query_params);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("logsource does not narrow the search, as the structure table warns", () => {
|
|
517
|
+
const withLogsource: Statement =
|
|
518
|
+
SigmaClickhouseCompiler.compileYaml(`title: T
|
|
519
|
+
logsource:
|
|
520
|
+
category: authentication
|
|
521
|
+
product: okta
|
|
522
|
+
detection:
|
|
523
|
+
selection:
|
|
524
|
+
className: Authentication
|
|
525
|
+
condition: selection
|
|
526
|
+
`);
|
|
527
|
+
const without: Statement = SigmaClickhouseCompiler.compileYaml(
|
|
528
|
+
ruleWithSelectionLine("className: Authentication"),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(withLogsource.query).toBe(without.query);
|
|
532
|
+
expect(withLogsource.query_params).toEqual(without.query_params);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("a missing or unknown level reads as the documented default", () => {
|
|
536
|
+
expect(
|
|
537
|
+
SigmaRuleParser.parse(ruleWithSelectionLine("className: X")).level,
|
|
538
|
+
).toBe(SIGMA_DEFAULT_LEVEL);
|
|
539
|
+
expect(
|
|
540
|
+
SigmaRuleParser.parse(
|
|
541
|
+
`level: severe\n${ruleWithSelectionLine("className: X")}`,
|
|
542
|
+
).level,
|
|
543
|
+
).toBe(SIGMA_DEFAULT_LEVEL);
|
|
544
|
+
expect(sectionMarkdown(DetectionRulesGuide, "writing-rules")).toContain(
|
|
545
|
+
`read as \`${SIGMA_DEFAULT_LEVEL}\``,
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe("Detection Rules guide: matching semantics", () => {
|
|
551
|
+
test("plain values ignore case", () => {
|
|
552
|
+
const statement: Statement = compileLine("message: 'Mimikatz Ran'");
|
|
553
|
+
|
|
554
|
+
expect(statement.query).toContain("lowerUTF8(message)");
|
|
555
|
+
expect(paramValues(statement)).toEqual(["mimikatz ran"]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("|cased matches case-sensitively", () => {
|
|
559
|
+
const statement: Statement = compileLine("message|cased: 'Mimikatz'");
|
|
560
|
+
|
|
561
|
+
expect(statement.query).not.toContain("lowerUTF8");
|
|
562
|
+
expect(paramValues(statement)).toEqual(["Mimikatz"]);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("regular expressions are case-sensitive (no case folding is applied)", () => {
|
|
566
|
+
const statement: Statement = compileLine("principalUser|re: '^Adm'");
|
|
567
|
+
|
|
568
|
+
expect(statement.query).toContain("match(principalUser");
|
|
569
|
+
expect(statement.query).not.toContain("lowerUTF8");
|
|
570
|
+
expect(statement.query).not.toContain("ILIKE");
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("* and ? are wildcards", () => {
|
|
574
|
+
const statement: Statement = compileLine("targetResource: '/tmp/*.sh'");
|
|
575
|
+
|
|
576
|
+
expect(statement.query).toContain("targetResource ILIKE");
|
|
577
|
+
expect(paramValues(statement)).toEqual(["/tmp/%.sh"]);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("null matches an empty column or a missing attribute", () => {
|
|
581
|
+
expect(compileLine("principalUser: null").query).toContain(
|
|
582
|
+
"principalUser = ''",
|
|
583
|
+
);
|
|
584
|
+
expect(compileLine("threat.matched: null").query).toContain(
|
|
585
|
+
"NOT mapContains(attributes",
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("list columns: a plain value needs exact membership, |contains matches inside items ignoring case", () => {
|
|
590
|
+
expect(compileLine("observables: 'Alice'").query).toContain(
|
|
591
|
+
"has(observables",
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const contains: Statement = compileLine("observables|contains: 'ali'");
|
|
595
|
+
expect(contains.query).toContain("arrayExists(x -> x ILIKE");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("any modifier outside the documented list is rejected at save", () => {
|
|
599
|
+
expect(() => {
|
|
600
|
+
SigmaRuleParser.parse(ruleWithSelectionLine("CommandLine|base64: 'x'"));
|
|
601
|
+
}).toThrow(/unsupported field modifier/);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("a nested map value is rejected at save, as troubleshooting says", () => {
|
|
605
|
+
expect(() => {
|
|
606
|
+
SigmaRuleParser.parse(`title: Nested
|
|
607
|
+
detection:
|
|
608
|
+
selection:
|
|
609
|
+
principal:
|
|
610
|
+
user: root
|
|
611
|
+
condition: selection
|
|
612
|
+
`);
|
|
613
|
+
}).toThrow(/nested object/);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("a condition naming a missing selection is rejected at save", () => {
|
|
617
|
+
expect(() => {
|
|
618
|
+
SigmaRuleParser.parse(`title: Missing
|
|
619
|
+
detection:
|
|
620
|
+
selection:
|
|
621
|
+
className: X
|
|
622
|
+
condition: selection and filter
|
|
623
|
+
`);
|
|
624
|
+
}).toThrow();
|
|
625
|
+
|
|
626
|
+
expect(() => {
|
|
627
|
+
SigmaRuleParser.parse(`title: No match
|
|
628
|
+
detection:
|
|
629
|
+
selection:
|
|
630
|
+
className: X
|
|
631
|
+
condition: 1 of filter_*
|
|
632
|
+
`);
|
|
633
|
+
}).toThrow(/matches no selection/);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("Detection Rules guide: the modifier table", () => {
|
|
638
|
+
test("lists exactly the modifiers the parser accepts — no more, no fewer", () => {
|
|
639
|
+
const documented: Array<string> = SIGMA_MODIFIER_DOCS.map(
|
|
640
|
+
(doc: SigmaModifierDoc) => {
|
|
641
|
+
return doc.modifier;
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
expect([...documented].sort()).toEqual(
|
|
646
|
+
[...SIGMA_SUPPORTED_MODIFIERS].sort(),
|
|
647
|
+
);
|
|
648
|
+
expect(new Set(documented).size).toBe(documented.length);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test.each(MODIFIER_CASES)(
|
|
652
|
+
"the %s example parses, uses its modifier, and compiles",
|
|
653
|
+
(_modifier: string, doc: SigmaModifierDoc) => {
|
|
654
|
+
const rule: SigmaRule = SigmaRuleParser.parse(
|
|
655
|
+
ruleWithSelectionLine(doc.example),
|
|
656
|
+
);
|
|
657
|
+
const requirement: SigmaFieldRequirement | undefined =
|
|
658
|
+
rule.selections[0]?.fieldMaps[0]?.[0];
|
|
659
|
+
|
|
660
|
+
expect(requirement?.modifiers).toContain(doc.modifier);
|
|
661
|
+
expect(() => {
|
|
662
|
+
SigmaClickhouseCompiler.compile(rule);
|
|
663
|
+
}).not.toThrow();
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
/*
|
|
668
|
+
* Each description promises a behavior; the compiled SQL is where that
|
|
669
|
+
* behavior lives.
|
|
670
|
+
*/
|
|
671
|
+
const EXPECTED_SQL: Record<string, (statement: Statement) => void> = {
|
|
672
|
+
contains: (statement: Statement) => {
|
|
673
|
+
expect(statement.query).toContain("ILIKE");
|
|
674
|
+
expect(paramValues(statement)).toEqual(["%-EncodedCommand%"]);
|
|
675
|
+
},
|
|
676
|
+
startswith: (statement: Statement) => {
|
|
677
|
+
expect(paramValues(statement)).toEqual(["10.%"]);
|
|
678
|
+
},
|
|
679
|
+
endswith: (statement: Statement) => {
|
|
680
|
+
expect(paramValues(statement)).toEqual(["%.ps1"]);
|
|
681
|
+
},
|
|
682
|
+
all: (statement: Statement) => {
|
|
683
|
+
expect(statement.query).toContain(" AND ");
|
|
684
|
+
expect(statement.query).not.toContain(" OR ");
|
|
685
|
+
},
|
|
686
|
+
re: (statement: Statement) => {
|
|
687
|
+
expect(statement.query).toContain("match(");
|
|
688
|
+
},
|
|
689
|
+
cased: (statement: Statement) => {
|
|
690
|
+
expect(statement.query).toContain(" LIKE ");
|
|
691
|
+
expect(statement.query).not.toContain("ILIKE");
|
|
692
|
+
},
|
|
693
|
+
gt: (statement: Statement) => {
|
|
694
|
+
expect(statement.query).toContain("severityId > ");
|
|
695
|
+
},
|
|
696
|
+
gte: (statement: Statement) => {
|
|
697
|
+
expect(statement.query).toContain("toFloat64OrNull(attributes[");
|
|
698
|
+
expect(statement.query).toContain(" >= ");
|
|
699
|
+
},
|
|
700
|
+
lt: (statement: Statement) => {
|
|
701
|
+
expect(statement.query).toContain("targetPort < ");
|
|
702
|
+
},
|
|
703
|
+
lte: (statement: Statement) => {
|
|
704
|
+
expect(statement.query).toContain("targetPort <= ");
|
|
705
|
+
},
|
|
706
|
+
cidr: (statement: Statement) => {
|
|
707
|
+
expect(statement.query).toContain("isIPAddressInRange(principalIp");
|
|
708
|
+
},
|
|
709
|
+
exists: (statement: Statement) => {
|
|
710
|
+
expect(statement.query).toContain("mapContains(attributes");
|
|
711
|
+
expect(statement.query).not.toContain("NOT mapContains");
|
|
712
|
+
},
|
|
713
|
+
windash: (statement: Statement) => {
|
|
714
|
+
expect(paramValues(statement)).toEqual(
|
|
715
|
+
expect.arrayContaining(["% -exec %", "% /exec %"]),
|
|
716
|
+
);
|
|
717
|
+
expect(statement.query).toContain(" OR ");
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
test("every modifier has a behavior check below", () => {
|
|
722
|
+
expect(Object.keys(EXPECTED_SQL).sort()).toEqual(
|
|
723
|
+
[...SIGMA_SUPPORTED_MODIFIERS].sort(),
|
|
724
|
+
);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test.each(MODIFIER_CASES)(
|
|
728
|
+
"the %s example compiles to what its description says",
|
|
729
|
+
(modifier: string, doc: SigmaModifierDoc) => {
|
|
730
|
+
EXPECTED_SQL[modifier]!(compileLine(doc.example));
|
|
731
|
+
},
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
test("the table is rendered into the Writing Rules section", () => {
|
|
735
|
+
const writing: string = sectionMarkdown(
|
|
736
|
+
DetectionRulesGuide,
|
|
737
|
+
"writing-rules",
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
for (const doc of SIGMA_MODIFIER_DOCS) {
|
|
741
|
+
expect(writing).toContain(`\`${doc.modifier}\``);
|
|
742
|
+
expect(writing).toContain(doc.example.replace(/\|/g, "\\|"));
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
describe("Detection Rules guide: field names", () => {
|
|
748
|
+
test.each(ALIAS_CASES)(
|
|
749
|
+
"%s resolves to %s",
|
|
750
|
+
(alias: string, column: string) => {
|
|
751
|
+
const resolved: ReturnType<typeof resolveSigmaField> =
|
|
752
|
+
resolveSigmaField(alias);
|
|
753
|
+
|
|
754
|
+
expect(resolved.kind).not.toBe("attribute");
|
|
755
|
+
expect((resolved as { column: string }).column).toBe(column);
|
|
756
|
+
},
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
test("every alias the compiler knows is documented", () => {
|
|
760
|
+
const documented: Set<string> = new Set(
|
|
761
|
+
SIGMA_FIELD_ALIAS_DOCS.flatMap((doc: SigmaFieldDoc) => {
|
|
762
|
+
return doc.aliases.map((alias: string) => {
|
|
763
|
+
return alias.toLowerCase();
|
|
764
|
+
});
|
|
765
|
+
}),
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
for (const alias of Object.keys(FIELD_ALIASES)) {
|
|
769
|
+
expect(documented).toContain(alias);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
expect(documented.size).toBe(Object.keys(FIELD_ALIASES).length);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test("every documented column is a typed column, reachable by its own name", () => {
|
|
776
|
+
const columns: Array<string> = SIGMA_COLUMN_DOCS.flatMap(
|
|
777
|
+
(entry: { columns: Array<string> }) => {
|
|
778
|
+
return entry.columns;
|
|
779
|
+
},
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
expect(new Set(columns).size).toBe(columns.length);
|
|
783
|
+
|
|
784
|
+
for (const column of columns) {
|
|
785
|
+
const resolved: ReturnType<typeof resolveSigmaField> =
|
|
786
|
+
resolveSigmaField(column);
|
|
787
|
+
expect(resolved.kind).not.toBe("attribute");
|
|
788
|
+
expect((resolved as { column: string }).column).toBe(column);
|
|
789
|
+
// "names are case-insensitive"
|
|
790
|
+
expect(
|
|
791
|
+
(resolveSigmaField(column.toUpperCase()) as { column: string }).column,
|
|
792
|
+
).toBe(column);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("the three columns the guide calls lists are list columns", () => {
|
|
797
|
+
for (const column of ["observables", "mitreTactics", "mitreTechniques"]) {
|
|
798
|
+
expect(resolveSigmaField(column).kind).toBe("arrayColumn");
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("any other name is an attribute key, spelled exactly — an attributes. prefix is NOT stripped", () => {
|
|
803
|
+
expect(resolveSigmaField("threat.matched")).toEqual({
|
|
804
|
+
kind: "attribute",
|
|
805
|
+
attributeKey: "threat.matched",
|
|
806
|
+
});
|
|
807
|
+
expect(resolveSigmaField("metadata.product.name")).toEqual({
|
|
808
|
+
kind: "attribute",
|
|
809
|
+
attributeKey: "metadata.product.name",
|
|
810
|
+
});
|
|
811
|
+
// The prefix would look up a key that does not exist.
|
|
812
|
+
expect(resolveSigmaField("attributes.threat.matched")).toEqual({
|
|
813
|
+
kind: "attribute",
|
|
814
|
+
attributeKey: "attributes.threat.matched",
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const writing: string = sectionMarkdown(
|
|
818
|
+
DetectionRulesGuide,
|
|
819
|
+
"writing-rules",
|
|
820
|
+
);
|
|
821
|
+
expect(writing).toContain("Do not add an `attributes.` prefix");
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
describe("Detection Rules guide: evaluation", () => {
|
|
826
|
+
test.each(THRESHOLD_CASES)(
|
|
827
|
+
"worked example %s uses real columns and a legal threshold",
|
|
828
|
+
(_goal: string, example: DetectionThresholdExample) => {
|
|
829
|
+
for (const field of [example.groupByField, example.distinctCountField]) {
|
|
830
|
+
if (field) {
|
|
831
|
+
expect(resolveSigmaField(field).kind).not.toBe("attribute");
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
expect(example.threshold).toBeGreaterThanOrEqual(
|
|
836
|
+
DETECTION_MATCH_COUNT_THRESHOLD_MIN,
|
|
837
|
+
);
|
|
838
|
+
expect(example.threshold).toBeLessThanOrEqual(
|
|
839
|
+
DETECTION_MATCH_COUNT_THRESHOLD_MAX,
|
|
840
|
+
);
|
|
841
|
+
},
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
test("distinct counting skips 0 in number columns, as the guide says of targetPort", () => {
|
|
845
|
+
expect(buildSigmaDistinctCountExpression("targetPort").query).toContain(
|
|
846
|
+
"nullIf(targetPort, 0)",
|
|
847
|
+
);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test("Group By accepts the same names as the rule itself", () => {
|
|
851
|
+
expect(buildSigmaFieldExpression("SourceIp").query).toBe("principalIp");
|
|
852
|
+
expect(buildSigmaFieldExpression("threat.feed").query).toContain(
|
|
853
|
+
"attributes[",
|
|
854
|
+
);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
test("states the interval bounds, the lookback cap, the group cap and the threshold range", () => {
|
|
858
|
+
const evaluation: string = sectionMarkdown(
|
|
859
|
+
DetectionRulesGuide,
|
|
860
|
+
"evaluation",
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
expect(evaluation).toContain(
|
|
864
|
+
`from ${DETECTION_EVALUATION_INTERVAL_MIN_IN_MINUTES} to ${DETECTION_EVALUATION_INTERVAL_MAX_IN_MINUTES}`,
|
|
865
|
+
);
|
|
866
|
+
expect(evaluation).toContain(
|
|
867
|
+
`last ${DETECTION_MAX_LOOKBACK_IN_MINUTES / 60} hours`,
|
|
868
|
+
);
|
|
869
|
+
expect(evaluation).toContain(
|
|
870
|
+
`At most ${DETECTION_MAX_GROUPS_PER_EVALUATION} groups`,
|
|
871
|
+
);
|
|
872
|
+
expect(evaluation).toContain(`\`${DETECTION_MATCH_COUNT_THRESHOLD_MIN}\``);
|
|
873
|
+
expect(evaluation).toContain(
|
|
874
|
+
DETECTION_MATCH_COUNT_THRESHOLD_MAX.toLocaleString("en-US"),
|
|
875
|
+
);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("the schedule step on the card states the interval bounds", () => {
|
|
879
|
+
expect(DetectionRulesGuide.steps[1]?.description).toContain(
|
|
880
|
+
`${DETECTION_EVALUATION_INTERVAL_MIN_IN_MINUTES}–${DETECTION_EVALUATION_INTERVAL_MAX_IN_MINUTES} minutes`,
|
|
881
|
+
);
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
describe("Detection Rules guide: severity levels", () => {
|
|
886
|
+
test("the card lists every Sigma level, in order, with the severity findings get", () => {
|
|
887
|
+
expect(
|
|
888
|
+
DetectionRulesGuide.levels.items.map((item: SecurityEventsGuideLevel) => {
|
|
889
|
+
return item.label;
|
|
890
|
+
}),
|
|
891
|
+
).toEqual(SIGMA_LEVELS_IN_ORDER);
|
|
892
|
+
|
|
893
|
+
for (const item of DetectionRulesGuide.levels.items) {
|
|
894
|
+
expect(item.severity).toBe(
|
|
895
|
+
SIGMA_LEVEL_TO_OCSF_SEVERITY[item.label as SigmaLevel],
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
expect(Object.values(SigmaLevel).sort()).toEqual(
|
|
900
|
+
[...SIGMA_LEVELS_IN_ORDER].sort(),
|
|
901
|
+
);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("the finding severity table matches the engine's mapping", () => {
|
|
905
|
+
const levels: string = sectionMarkdown(
|
|
906
|
+
DetectionRulesGuide,
|
|
907
|
+
"severity-levels",
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
for (const level of SIGMA_LEVELS_IN_ORDER) {
|
|
911
|
+
expect(levels).toContain(
|
|
912
|
+
`| \`${level}\` | ${SIGMA_LEVEL_TO_OCSF_SEVERITY[level]} |`,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test("the rank fallback splits the levels exactly where the engine does", () => {
|
|
918
|
+
const levels: string = sectionMarkdown(
|
|
919
|
+
DetectionRulesGuide,
|
|
920
|
+
"severity-levels",
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
expect(levels).toContain(
|
|
924
|
+
"Otherwise `high` and `critical` rules use your **most severe** alert severity",
|
|
925
|
+
);
|
|
926
|
+
expect(levels).toContain(
|
|
927
|
+
"`informational`, `low` and `medium` rules your **least severe**",
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
for (const level of SIGMA_LEVELS_IN_ORDER) {
|
|
931
|
+
expect(isSevereSigmaLevel(level)).toBe(
|
|
932
|
+
level === SigmaLevel.High || level === SigmaLevel.Critical,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test("the documented precedence is the one the alerting code applies", () => {
|
|
938
|
+
type Severity = { id: ObjectID; name: string };
|
|
939
|
+
|
|
940
|
+
const mostSevere: Severity = {
|
|
941
|
+
id: new ObjectID("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"),
|
|
942
|
+
name: "P1",
|
|
943
|
+
};
|
|
944
|
+
const named: Severity = {
|
|
945
|
+
id: new ObjectID("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
|
946
|
+
name: "HIGH",
|
|
947
|
+
};
|
|
948
|
+
const leastSevere: Severity = {
|
|
949
|
+
id: new ObjectID("cccccccc-cccc-4ccc-8ccc-cccccccccccc"),
|
|
950
|
+
name: "P4",
|
|
951
|
+
};
|
|
952
|
+
const severities: Array<Severity> = [mostSevere, named, leastSevere];
|
|
953
|
+
|
|
954
|
+
// 1. The rule's own severity wins over a name match.
|
|
955
|
+
expect(
|
|
956
|
+
pickSeverityByPrecedence({
|
|
957
|
+
severities,
|
|
958
|
+
explicitSeverityId: leastSevere.id,
|
|
959
|
+
severityLabel: SigmaLevel.High,
|
|
960
|
+
isSevere: true,
|
|
961
|
+
})?.toString(),
|
|
962
|
+
).toBe(leastSevere.id.toString());
|
|
963
|
+
|
|
964
|
+
// 2. A severity named after the level, ignoring case.
|
|
965
|
+
expect(
|
|
966
|
+
pickSeverityByPrecedence({
|
|
967
|
+
severities,
|
|
968
|
+
explicitSeverityId: undefined,
|
|
969
|
+
severityLabel: SigmaLevel.High,
|
|
970
|
+
isSevere: isSevereSigmaLevel(SigmaLevel.High),
|
|
971
|
+
})?.toString(),
|
|
972
|
+
).toBe(named.id.toString());
|
|
973
|
+
|
|
974
|
+
// 3. By rank.
|
|
975
|
+
for (const level of SIGMA_LEVELS_IN_ORDER.filter(
|
|
976
|
+
(candidate: SigmaLevel) => {
|
|
977
|
+
return candidate !== SigmaLevel.High;
|
|
978
|
+
},
|
|
979
|
+
)) {
|
|
980
|
+
const expected: Severity = isSevereSigmaLevel(level)
|
|
981
|
+
? mostSevere
|
|
982
|
+
: leastSevere;
|
|
983
|
+
|
|
984
|
+
expect(
|
|
985
|
+
pickSeverityByPrecedence({
|
|
986
|
+
severities,
|
|
987
|
+
explicitSeverityId: undefined,
|
|
988
|
+
severityLabel: level,
|
|
989
|
+
isSevere: isSevereSigmaLevel(level),
|
|
990
|
+
})?.toString(),
|
|
991
|
+
).toBe(expected.id.toString());
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
describe("Detection Rules guide: alerts and findings", () => {
|
|
997
|
+
test("names every attribute a finding carries, and its class and product", () => {
|
|
998
|
+
const findings: string = sectionMarkdown(
|
|
999
|
+
DetectionRulesGuide,
|
|
1000
|
+
"alerts-and-findings",
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
for (const attribute of [
|
|
1004
|
+
DETECTION_RULE_ID_ATTRIBUTE,
|
|
1005
|
+
DETECTION_RULE_NAME_ATTRIBUTE,
|
|
1006
|
+
DETECTION_MATCH_COUNT_ATTRIBUTE,
|
|
1007
|
+
DETECTION_DISTINCT_COUNT_ATTRIBUTE,
|
|
1008
|
+
DETECTION_GROUP_VALUE_ATTRIBUTE,
|
|
1009
|
+
DETECTION_SIGMA_ID_ATTRIBUTE,
|
|
1010
|
+
]) {
|
|
1011
|
+
expect(findings).toContain(`\`${attribute}\``);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
expect(findings).toContain(`\`${DETECTION_FINDING_CLASS_NAME}\``);
|
|
1015
|
+
expect(findings).toContain(`OCSF ${DETECTION_FINDING_CLASS_UID}`);
|
|
1016
|
+
expect(findings).toContain("`OneUptime Detections`");
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
test("the alert title format matches the engine's", () => {
|
|
1020
|
+
const findings: string = sectionMarkdown(
|
|
1021
|
+
DetectionRulesGuide,
|
|
1022
|
+
"alerts-and-findings",
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
expect(findings).toContain("`[Detection] <rule name> — <group value>`");
|
|
1026
|
+
expect(findings).toContain("`[Detection] <rule name>`");
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
test("no longer tells anyone to prefix attribute keys with attributes.", () => {
|
|
1030
|
+
expect(guideToMarkdown(DetectionRulesGuide)).not.toContain(
|
|
1031
|
+
"`attributes.<key>`",
|
|
1032
|
+
);
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
describe("Threat Intel guide: threat levels", () => {
|
|
1037
|
+
test("the confidence bands tile 1–100 with no gap or overlap", () => {
|
|
1038
|
+
const covered: Array<number> = [];
|
|
1039
|
+
|
|
1040
|
+
for (const band of THREAT_LEVEL_BANDS) {
|
|
1041
|
+
expect(band.minimumConfidence).toBeLessThanOrEqual(
|
|
1042
|
+
band.maximumConfidence,
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
for (
|
|
1046
|
+
let confidence: number = band.minimumConfidence;
|
|
1047
|
+
confidence <= band.maximumConfidence;
|
|
1048
|
+
confidence++
|
|
1049
|
+
) {
|
|
1050
|
+
covered.push(confidence);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
covered.sort((a: number, b: number) => {
|
|
1055
|
+
return a - b;
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
const expected: Array<number> = [];
|
|
1059
|
+
for (let confidence: number = 1; confidence <= 100; confidence++) {
|
|
1060
|
+
expected.push(confidence);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
expect(covered).toEqual(expected);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
test.each(BAND_CASES)(
|
|
1067
|
+
"every confidence in %s maps to the band's threat level",
|
|
1068
|
+
(_label: string, band: ThreatLevelBand) => {
|
|
1069
|
+
const level: OcsfSeverity = threatLevelForBand(band);
|
|
1070
|
+
|
|
1071
|
+
for (
|
|
1072
|
+
let confidence: number = band.minimumConfidence;
|
|
1073
|
+
confidence <= band.maximumConfidence;
|
|
1074
|
+
confidence++
|
|
1075
|
+
) {
|
|
1076
|
+
expect(ocsfSeverityForConfidence(confidence)).toBe(level);
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
test("adjacent bands are different levels, so no band could be merged away", () => {
|
|
1082
|
+
for (let index: number = 1; index < THREAT_LEVEL_BANDS.length; index++) {
|
|
1083
|
+
expect(threatLevelForBand(THREAT_LEVEL_BANDS[index]!)).not.toBe(
|
|
1084
|
+
threatLevelForBand(THREAT_LEVEL_BANDS[index - 1]!),
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
test("the bands read Critical, High, Medium, Low from the top", () => {
|
|
1090
|
+
expect(THREAT_LEVEL_BANDS.map(threatLevelForBand)).toEqual([
|
|
1091
|
+
OcsfSeverity.Critical,
|
|
1092
|
+
OcsfSeverity.High,
|
|
1093
|
+
OcsfSeverity.Medium,
|
|
1094
|
+
OcsfSeverity.Low,
|
|
1095
|
+
]);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test("unscored — 0 or missing — reads as the level the card shows", () => {
|
|
1099
|
+
const unscored: SecurityEventsGuideLevel | undefined =
|
|
1100
|
+
ThreatIntelGuide.levels.items[ThreatIntelGuide.levels.items.length - 1];
|
|
1101
|
+
|
|
1102
|
+
expect(unscored?.label).toBe("Not scored");
|
|
1103
|
+
expect(ocsfSeverityForConfidence(UNSCORED_CONFIDENCE)).toBe(
|
|
1104
|
+
unscored?.severity,
|
|
1105
|
+
);
|
|
1106
|
+
expect(ocsfSeverityForConfidence(Number.NaN)).toBe(unscored?.severity);
|
|
1107
|
+
expect(unscored?.severity).toBe(OcsfSeverity.Medium);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
test("the card lists one chip per band plus unscored", () => {
|
|
1111
|
+
expect(ThreatIntelGuide.levels.items).toHaveLength(
|
|
1112
|
+
THREAT_LEVEL_BANDS.length + 1,
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
THREAT_LEVEL_BANDS.forEach((band: ThreatLevelBand, index: number) => {
|
|
1116
|
+
expect(ThreatIntelGuide.levels.items[index]).toEqual({
|
|
1117
|
+
label: `Confidence ${formatBand(band)}`,
|
|
1118
|
+
severity: threatLevelForBand(band),
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test("the Threat Levels tab tabulates every band", () => {
|
|
1124
|
+
const levels: string = sectionMarkdown(ThreatIntelGuide, "threat-levels");
|
|
1125
|
+
|
|
1126
|
+
for (const band of THREAT_LEVEL_BANDS) {
|
|
1127
|
+
expect(levels).toContain(
|
|
1128
|
+
`| ${formatBand(band)} | ${threatLevelForBand(band)} |`,
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
expect(levels).toContain(
|
|
1133
|
+
`| Not scored (${UNSCORED_CONFIDENCE} or missing) | ${OcsfSeverity.Medium} |`,
|
|
1134
|
+
);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
test("a Minimum Confidence of 70 keeps only High and Critical, as the example says", () => {
|
|
1138
|
+
for (let confidence: number = 1; confidence <= 100; confidence++) {
|
|
1139
|
+
const level: OcsfSeverity = ocsfSeverityForConfidence(confidence);
|
|
1140
|
+
const isHighOrCritical: boolean =
|
|
1141
|
+
level === OcsfSeverity.High || level === OcsfSeverity.Critical;
|
|
1142
|
+
|
|
1143
|
+
expect(isHighOrCritical).toBe(confidence >= 70);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
expect(sectionMarkdown(ThreatIntelGuide, "threat-levels")).toContain(
|
|
1147
|
+
"a minimum of 70 keeps only High and Critical",
|
|
1148
|
+
);
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
describe("Threat Intel guide: indicators", () => {
|
|
1153
|
+
test("documents every indicator type the parser can produce", () => {
|
|
1154
|
+
expect(Object.keys(INDICATOR_TYPE_DOCS).sort()).toEqual(
|
|
1155
|
+
Object.values(ThreatIntelIndicatorType).sort(),
|
|
1156
|
+
);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
test.each(Object.values(ThreatIntelIndicatorType))(
|
|
1160
|
+
"the %s example pattern parses to exactly that type",
|
|
1161
|
+
(type: ThreatIntelIndicatorType) => {
|
|
1162
|
+
const parsed: Array<ParsedIndicatorValue> | null =
|
|
1163
|
+
StixPatternParser.parse(INDICATOR_TYPE_DOCS[type].examplePattern);
|
|
1164
|
+
|
|
1165
|
+
expect(parsed).not.toBeNull();
|
|
1166
|
+
expect(parsed).toHaveLength(1);
|
|
1167
|
+
expect(parsed![0]!.type).toBe(type);
|
|
1168
|
+
// "Values are stored in lowercase"
|
|
1169
|
+
expect(parsed![0]!.value).toBe(parsed![0]!.value.toLowerCase());
|
|
1170
|
+
},
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
test.each(SUPPORTED_COMBINED_PATTERN_EXAMPLES)(
|
|
1174
|
+
"the OR example %s is supported and yields both values",
|
|
1175
|
+
(pattern: string) => {
|
|
1176
|
+
expect(StixPatternParser.parse(pattern)).toHaveLength(2);
|
|
1177
|
+
},
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
test.each(UNSUPPORTED_PATTERN_EXAMPLES)(
|
|
1181
|
+
"the unsupported example %s is skipped whole",
|
|
1182
|
+
(pattern: string) => {
|
|
1183
|
+
expect(StixPatternParser.parse(pattern)).toBeNull();
|
|
1184
|
+
},
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
test("the Indicators tab shows every example and the default validity", () => {
|
|
1188
|
+
const indicators: string = sectionMarkdown(ThreatIntelGuide, "indicators");
|
|
1189
|
+
|
|
1190
|
+
for (const type of Object.values(ThreatIntelIndicatorType)) {
|
|
1191
|
+
expect(indicators).toContain(INDICATOR_TYPE_DOCS[type].examplePattern);
|
|
1192
|
+
expect(indicators).toContain(`\`${type}\``);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
for (const pattern of SUPPORTED_COMBINED_PATTERN_EXAMPLES) {
|
|
1196
|
+
expect(indicators).toContain(pattern);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
expect(indicators).toContain(`${THREAT_INTEL_DEFAULT_VALID_DAYS} days`);
|
|
1200
|
+
expect(indicators).toContain(`\`${THREAT_INTEL_MINIMUM_CONFIDENCE_MIN}\``);
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
describe("Threat Intel guide: enrichment and matching", () => {
|
|
1205
|
+
test("the detection rule sample compiles and reads threat.* as attributes", () => {
|
|
1206
|
+
const rule: SigmaRule = SigmaRuleParser.parse(
|
|
1207
|
+
THREAT_INTEL_SIGMA_EXAMPLE_YAML,
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
expect(rule.level).toBe(SigmaLevel.Critical);
|
|
1211
|
+
expect(resolveSigmaField(ENRICHMENT_MATCHED_ATTRIBUTE).kind).toBe(
|
|
1212
|
+
"attribute",
|
|
1213
|
+
);
|
|
1214
|
+
expect(resolveSigmaField(ENRICHMENT_CONFIDENCE_ATTRIBUTE).kind).toBe(
|
|
1215
|
+
"attribute",
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
const statement: Statement = SigmaClickhouseCompiler.compile(rule);
|
|
1219
|
+
|
|
1220
|
+
expect(statement.query).toContain("toFloat64OrNull(attributes[");
|
|
1221
|
+
expect(paramValues(statement)).toEqual(
|
|
1222
|
+
expect.arrayContaining([
|
|
1223
|
+
ENRICHMENT_MATCHED_ATTRIBUTE,
|
|
1224
|
+
ENRICHMENT_CONFIDENCE_ATTRIBUTE,
|
|
1225
|
+
]),
|
|
1226
|
+
);
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
test("names every attribute stamped at ingest", () => {
|
|
1230
|
+
const enrichment: string = sectionMarkdown(
|
|
1231
|
+
ThreatIntelGuide,
|
|
1232
|
+
"enrichment-and-matching",
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
for (const attribute of [
|
|
1236
|
+
ENRICHMENT_MATCHED_ATTRIBUTE,
|
|
1237
|
+
ENRICHMENT_INDICATOR_ID_ATTRIBUTE,
|
|
1238
|
+
ENRICHMENT_INDICATOR_TYPE_ATTRIBUTE,
|
|
1239
|
+
ENRICHMENT_INDICATOR_VALUE_ATTRIBUTE,
|
|
1240
|
+
ENRICHMENT_FEED_ATTRIBUTE,
|
|
1241
|
+
ENRICHMENT_FEED_ID_ATTRIBUTE,
|
|
1242
|
+
ENRICHMENT_CONFIDENCE_ATTRIBUTE,
|
|
1243
|
+
ENRICHMENT_MATCH_COUNT_ATTRIBUTE,
|
|
1244
|
+
]) {
|
|
1245
|
+
expect(enrichment).toContain(`\`${attribute}\``);
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
test("names every attribute a Threat Intel finding carries, and its class and product", () => {
|
|
1250
|
+
const enrichment: string = sectionMarkdown(
|
|
1251
|
+
ThreatIntelGuide,
|
|
1252
|
+
"enrichment-and-matching",
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
for (const attribute of [
|
|
1256
|
+
THREAT_FEED_ID_ATTRIBUTE,
|
|
1257
|
+
THREAT_FEED_NAME_ATTRIBUTE,
|
|
1258
|
+
THREAT_INDICATOR_ID_ATTRIBUTE,
|
|
1259
|
+
THREAT_INDICATOR_TYPE_ATTRIBUTE,
|
|
1260
|
+
THREAT_INDICATOR_VALUE_ATTRIBUTE,
|
|
1261
|
+
THREAT_CONFIDENCE_ATTRIBUTE,
|
|
1262
|
+
THREAT_MATCH_COUNT_ATTRIBUTE,
|
|
1263
|
+
]) {
|
|
1264
|
+
expect(enrichment).toContain(`\`${attribute}\``);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
expect(enrichment).toContain(`\`${DETECTION_FINDING_CLASS_NAME}\``);
|
|
1268
|
+
expect(enrichment).toContain(`OCSF ${DETECTION_FINDING_CLASS_UID}`);
|
|
1269
|
+
expect(enrichment).toContain(`\`${THREAT_INTEL_PRODUCT_NAME}\``);
|
|
1270
|
+
expect(enrichment).toContain(
|
|
1271
|
+
"`[Threat Intel] <feed name> — <indicator value>`",
|
|
1272
|
+
);
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
test("states the matcher's first window, lookback cap and per-check cap", () => {
|
|
1276
|
+
const enrichment: string = sectionMarkdown(
|
|
1277
|
+
ThreatIntelGuide,
|
|
1278
|
+
"enrichment-and-matching",
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
expect(enrichment).toContain(
|
|
1282
|
+
`last ${THREAT_INTEL_FIRST_MATCH_WINDOW_IN_MINUTES} minutes`,
|
|
1283
|
+
);
|
|
1284
|
+
expect(enrichment).toContain(
|
|
1285
|
+
`last ${THREAT_INTEL_MATCH_MAX_LOOKBACK_IN_MINUTES / 60} hours`,
|
|
1286
|
+
);
|
|
1287
|
+
expect(enrichment).toContain(
|
|
1288
|
+
`at most ${THREAT_INTEL_MAX_INDICATORS_PER_EVALUATION} indicator values`,
|
|
1289
|
+
);
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
describe("Threat Intel guide: feeds and polling", () => {
|
|
1294
|
+
test("states the poll interval bounds and default", () => {
|
|
1295
|
+
const feeds: string = sectionMarkdown(
|
|
1296
|
+
ThreatIntelGuide,
|
|
1297
|
+
"feeds-and-polling",
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
expect(feeds).toContain(
|
|
1301
|
+
`${THREAT_INTEL_POLL_INTERVAL_MIN_IN_MINUTES}–${THREAT_INTEL_POLL_INTERVAL_MAX_IN_MINUTES}, default ${THREAT_INTEL_DEFAULT_POLL_INTERVAL_IN_MINUTES}`,
|
|
1302
|
+
);
|
|
1303
|
+
expect(feeds).toContain(
|
|
1304
|
+
`${THREAT_INTEL_MINIMUM_CONFIDENCE_MIN}–${THREAT_INTEL_MINIMUM_CONFIDENCE_MAX}`,
|
|
1305
|
+
);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
test("the polling step on the card states the same bounds and default", () => {
|
|
1309
|
+
expect(ThreatIntelGuide.steps[0]?.description).toContain(
|
|
1310
|
+
`${THREAT_INTEL_POLL_INTERVAL_MIN_IN_MINUTES}–${THREAT_INTEL_POLL_INTERVAL_MAX_IN_MINUTES} minutes (default ${THREAT_INTEL_DEFAULT_POLL_INTERVAL_IN_MINUTES})`,
|
|
1311
|
+
);
|
|
1312
|
+
});
|
|
1313
|
+
});
|